{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleContexts #-}

module Data.Var
  (
  -- * Different types of variables
    Sig
  , newSig
  , Ref
  , newRef
  , Var
  , newVar

  -- * Generic operations
  , Settable
  , set
  , Gettable
  , get
  , modify
  , modifyWith
  , Subscribable
  , subscribe
  , withUnsubscriber

  -- * Specific operations
  , subscribeWithOld
  , subscribeChange
  , subscribeAndRead
  , subscribeChangeAndRead
  , subscribeExclusive
  , subscribeAndReadExclusive
  , mapVar
  , mergeVars
  , mergeVars'
  , tupleVars
  , tupleVars'
  , waitForN
  , waitFor
  , oneShot
  , holdSig

  ) where

import Data.Maybe
import FFI
import Prelude

-- | A subscribable signal.  Can have handlers subscribed to them, but doesn't
--   store a value.
data Sig a

-- | Make a new signal.
newSig :: Fay (Ptr (Sig a))
newSig = ffi "new Fay$$Sig()"

-- | A mutable reference, with no subscribers.
data Ref a

-- | Make a new mutable reference.
newRef :: Ptr a -> Fay (Ptr (Ref a))
newRef = ffi "new Fay$$Ref2(%1)"

-- | A reactive variable.  Stores a value, and can have handlers subscribed to
--   changes.
data Var a

-- | Make a new reactive variable.
newVar :: Ptr a -> Fay (Ptr (Var a))
newVar = ffi "new Fay$$Var(%1)"


-- | All of the variable types can be set to a value.
class Settable v
instance Settable (Ref a)
instance Settable (Sig a)
instance Settable (Var a)

-- | Write to the value (if any), and call subscribers (if any).
set :: Settable (v a) => Ptr (v a) -> Ptr a -> Fay ()
set = ffi "Fay$$setValue(Fay$$_(%1), %2, Fay$$_)"


-- | 'Ref' and 'Var' store their last set value.
class Gettable v
instance Gettable (Ref a)
instance Gettable (Var a)

-- | Get the value of a 'Ref' or 'Var'.
get :: Gettable (v a) => Ptr (v a) -> Fay (Ptr a)
get = ffi "Fay$$_(%1).val"

-- | Modifies the current value with a pure function.
modify :: (Settable (v a), Gettable (v a)) => v a -> (a -> a) -> Fay ()
modify v f = get v >>= set v . f

-- | Runs a 'Fay' action on the current value, and updates with the result.
modifyWith :: (Settable (v a), Gettable (v a)) => v a -> (a -> Fay a) -> Fay ()
modifyWith v f = get v >>= f >>= set v

-- | 'Sig' and 'Var' have lists of subscribers that are notified when 'set' is
--   used.
class Settable v => Subscribable v
instance Subscribable (Sig a)
instance Subscribable (Var a)

-- | Subscribe to the value of a 'Sig' or 'Var'.
--
--   The result is an unsubscribe function.
subscribe :: Subscribable (v a) => Ptr (v a) -> Ptr (a -> Fay void) -> Fay (() -> Fay ())
subscribe = ffi "Fay$$subscribe(Fay$$_(%1), Fay$$_(%2))"

-- | Run the same subscribing action but provide an additional
-- unsubscribe parameter to the handler.
withUnsubscriber :: ((a -> Fay ()) -> Fay (() -> Fay ()))
                 -> (((() -> Fay ()) -> a -> Fay ()) -> Fay (() -> Fay ()))
withUnsubscriber f = \g -> do
  unsubscriber <- newRef Nothing
  unsubscribe <- f $ \v -> do munsubscriber <- get unsubscriber
                              whenJust munsubscriber $ \unsubscribe -> g unsubscribe v
  set unsubscriber (Just unsubscribe)
  return unsubscribe

-- | Subscribe to a 'Var', along with the previous value.
--
--   The result is an unsubscribe function.
subscribeWithOld :: Var a -> (a -> a -> Fay ()) -> Fay (() -> Fay ())
subscribeWithOld v f = do
  o <- get v >>= newRef
  subscribe v $ \x' -> do
    x <- get o
    set o x'
    f x x'

-- | Subscribe to a 'Var', but only call handler when it actually changes.
--
--   The result is an unsubscribe function.
subscribeChange :: Eq a => Var a -> (a -> Fay ()) -> Fay (() -> Fay ())
subscribeChange v f = subscribeWithOld v $ \x x' -> when (x /= x') $ f x'

-- | Subscribe to a 'Var', and call the function on the current value.
--
--   The result is an unsubscribe function.
subscribeAndRead :: Var a -> (a -> Fay void) -> Fay (() -> Fay ())
subscribeAndRead v f = do
  x <- get v
  _ <- f x
  subscribe v f

-- | Subscribe to a 'Var', but only call handler when it actually changes, and
--   also initially on registration.
--
--   The result is an unsubscribe function.
subscribeChangeAndRead :: Eq a => Var a -> (a -> Fay ()) -> Fay (() -> Fay ())
subscribeChangeAndRead v f = do
  x <- get v
  f x
  subscribeChange v f


-- | Given a change handler, returns a function that can be used to set a
--   subscribable without invoking the handler.  This can be useful in
--   situations where the handler for a 'Var' causes an event which otherwise
--   ought to set the value of the 'Var'.  An example of this is interfacing
--   with HTML input field change events.
--
--   The 'snd' part of the result is an unsubscribe function.
subscribeExclusive :: Subscribable (v a) => v a -> (a -> Fay ()) -> Fay (a -> Fay (), () -> Fay ())
subscribeExclusive v onChange = do
  bracket <- getBracket
  unsubscribe <- subscribe v $ bracket . onChange
  return (\x -> bracket $ set v x, unsubscribe)

-- | Given a change handler, returns a function that can be used to set a var
--   without invoking the handler. The handler is called with the initial
--   value. This can be useful in situations where the handler for a 'Var'
--   causes an event which otherwise ought to set the value of the 'Var'.  An
--   example of this is interfacing with HTML input field change events.
--
--   The 'snd' part of the result is an unsubscribe function.
subscribeAndReadExclusive :: Var a -> (a -> Fay ()) -> Fay (a -> Fay (), () -> Fay ())
subscribeAndReadExclusive v onChange = do
  bracket <- getBracket
  unsubscribe <- subscribeAndRead v $ bracket . onChange
  return (\x -> bracket $ set v x, unsubscribe)

-- Utility used for 'subscribeExclusive', 'subscribeAndReadExclusive', and
-- 'mergeVars'.
getBracket :: Fay (Fay () -> Fay ())
getBracket = do
  rhandle <- newRef True
  return $ \f -> do
    handle <- get rhandle
    when handle $ do
      set rhandle False
      f
      set rhandle True

--TODO: mapVar variant that's bidirectional?
--TODO: return unsubscribe?

-- | Creates a 'Var' that updates whenever the source var is changed, applying
--   the provided function to compute the new value.
mapVar :: (a -> b) -> Var a -> Fay (Var b)
mapVar f v = do
  x <- get v
  r <- newVar (f x)
  _ <- subscribe v $ \x' -> set r $ f x'
  return r

-- | Creates a 'Var' that updates whenever one of its source vars are changed.
--   If the 2nd argument is a 'Just' value, then its used to set the source
--   vars when the variable is changed. Setting using a merged var is
--   sometimes preferred because both values are set before the subscribers
--   are called.
--
--   The 'snd' part of the result is an unsubscribe function.
mergeVars :: (a -> b -> c) -> Maybe (c -> (a, b)) -> Var a -> Var b
          -> Fay (Var c, Fay ())
mergeVars f mg va vb = do
  bracket <- getBracket
  a0 <- get va
  b0 <- get vb
  vc <- newVar (f a0 b0)
  unsubscribeA <- subscribe va $ \a -> bracket $ do
    b <- get vb
    set vc (f a b)
  unsubscribeB <- subscribe vb $ \b -> bracket $ do
    a <- get va
    set vc (f a b)
  unsubscribe <- case mg of
    Nothing -> return $ unsubscribeA () >> unsubscribeB ()
    Just g -> do
      unsubscribeC <- subscribe vc $ \c -> bracket $ case g c of
        (a, b) -> do
          -- Set variables before broadcast.
          setInternal va a
          setInternal vb b
          broadcastInternal va a
          broadcastInternal vb b
      return $ unsubscribeA () >> unsubscribeB () >> unsubscribeC ()
  return (vc, unsubscribe)

setInternal :: Ptr (Var a) -> Ptr a -> Fay ()
setInternal = ffi "function() { Fay$$_(%1).val = %2; }()"

broadcastInternal :: Ptr (Var a) -> Ptr a -> Fay ()
broadcastInternal = ffi "Fay$$broadcastInternal(Fay$$_(%1), %2, Fay$$_)"

-- | Like 'mergeVars', but discards the unsubscribe function.
mergeVars' :: (a -> b -> c) -> Maybe (c -> (a, b)) -> Var a -> Var b
           -> Fay (Var c)
mergeVars' f mg va vb = do
  result <- mergeVars f mg va vb
  case result of
    (v, _) -> return v

-- | Creates a 'Var' that updates whenever one of its source vars are changed.
--   It can also be used to set both source vars at once.
--
--   See 'mergeVars' for more information.  Note that when using nested tuples,
--   if you want all of the values to be set before broadcast, then they should
--   nest to the left.
tupleVars :: Var a -> Var b -> Fay (Var (a, b), Fay ())
tupleVars = mergeVars (\x y -> (x, y)) (Just id)

-- | Like 'tupleVars', but discards the unsubscribe function.
tupleVars' :: Var a -> Var b -> Fay (Var (a, b))
tupleVars' va vb = do
  result <- tupleVars va vb
  case result of
    (v, _) -> return v

-- | Wait for n signals on the given signaller.
waitForN :: Int -> Fay (Fay void -> Fay (),Sig ())
waitForN n = do
  sig <- newSig
  count <- newVar (0 :: Int)
  _ <- subscribe sig (const (modify count (+1)))
  return (\m -> subscribeAndRead count (\i -> when (i == n) (m >> return ())) >> return (),sig)

-- | Wait for the given predicate to be satisfied on the var and then
-- unsubscribe.
waitFor :: Var a -> (a -> Bool) -> (a -> Fay ()) -> Fay ()
waitFor v p f = do
  _ <- withUnsubscriber (subscribeAndRead v)
    $ \unsubscribe x -> when (p x) $ unsubscribe () >> f x
  return ()

-- | Make a one-shot variable subscription that immediately
-- unsubscribes after the event has triggered.
oneShot :: Subscribable (v a) => v a -> (a -> Fay ()) -> Fay ()
oneShot v f = do
  _ <- withUnsubscriber (subscribe v) $ \unsubscribe x -> unsubscribe () >> f x
  return ()

-- | Turn a sig into a var, by storing the last reported value.
holdSig :: a -> Sig a -> Fay (Var a)
holdSig initial sig = do
  v <- newVar initial
  void $ subscribe sig $ set v
  return v