-- |
-- Module:     FRP.NetWire.Request
-- Copyright:  (c) 2011 Ertugrul Soeylemez
-- License:    BSD3
-- Maintainer: Ertugrul Soeylemez <es@ertes.de>
--
-- Object managers, unique identifiers and context-sensitive wires.

module FRP.NetWire.Request
    ( -- * Containers
      MgrMsg(..),
      manager,

      -- * Context-sensitive mutation
      context,
      contextInt,
      contextLimited,
      contextLimitedInt,
      -- ** Simple variants
      context_,
      contextInt_,
      contextLimited_,
      contextLimitedInt_,

      -- * Identifiers.
      identifier
    )
    where

import qualified Data.IntMap as IM
import qualified Data.Map as M
import qualified Data.Traversable as T
import Control.Arrow
import Control.Monad.IO.Class
import Control.Concurrent.STM
import Data.IntMap (IntMap)
import Data.Map (Map)
import Data.Monoid
import FRP.NetWire.Wire


-- | Messages to wire managers (see the 'manager' wire).

data MgrMsg k m a b
    -- | Do nothing.  Send this, if the wire shouldn't be changed in an
    -- instant.
    = MgrNop

    -- | Perform two operations in an instant.
    | MgrMulti (MgrMsg k m a b) (MgrMsg k m a b)

    -- | Add the given wire with the given key.  If the manager already
    -- has a wire with this key, it is overwritten.
    | MgrAdd k (Wire m a b)

    -- | Delete the wire with the given key, if it exists.
    | MgrDel k

-- | The monoid instance can be used to combine multiple manager
-- operations.  They are performed from left to right.  This instance
-- tries hard to optimize operations away without sacrificing
-- performance.

instance Eq k => Monoid (MgrMsg k m a b) where
    mempty = MgrNop

    mappend MgrNop y = y
    mappend x MgrNop = x
    mappend (MgrAdd k1 _) y@(MgrAdd k2 _) | k1 == k2 = y
    mappend (MgrDel k1)   y@(MgrAdd k2 _) | k1 == k2 = y
    mappend (MgrAdd k1 _)   (MgrDel k2)   | k1 == k2 = MgrNop
    mappend (MgrDel k1)   y@(MgrDel k2)   | k1 == k2 = y
    mappend x y = MgrMulti x y


-- | Make the given wire context-sensitive.  The left input signal is a
-- context and the argument wire will mutate individually for each such
-- context.
--
-- Inherits inhibition and feedback behaviour from the current context's
-- wire.

context :: forall a b ctx m. (Ord ctx, Monad m) => Wire m (ctx, a) b -> Wire m (ctx, a) b
context w0 = context' M.empty 0
    where
    context' :: Map ctx (Time, Wire m (ctx, a) b) -> Time -> Wire m (ctx, a) b
    context' tm' t' =
        mkGen $ \ws@(wsDTime -> dt') inp@(ctx, _) -> do
            let t = t' + dt'
            let (dt, w') = case M.lookup ctx tm' of
                             Nothing       -> (0, w0)
                             Just (lt, w') -> (t - lt, w')
            (mx, w) <- dt `seq` toGen w' (ws { wsDTime = dt }) inp
            let tm = M.insert ctx (t, w) tm'
            return (mx, context' tm t)


-- | Simplified variant of 'context'.  Takes a context signal only.

context_ :: (Ord ctx, Monad m) => Wire m ctx b -> Wire m ctx b
context_ w0 = arr (, ()) >>> context (arr fst >>> w0)


-- | Specialized version of 'context'.  Use this one, if your contexts
-- are 'Int's and you have a lot of them.
--
-- Inherits inhibition and feedback behaviour from the current context's
-- wire.

contextInt :: forall a b m. Monad m => Wire m (Int, a) b -> Wire m (Int, a) b
contextInt w0 = context' IM.empty 0
    where
    context' :: IntMap (Time, Wire m (Int, a) b) -> Time -> Wire m (Int, a) b
    context' tm' t' =
        mkGen $ \ws@(wsDTime -> dt') inp@(ctx, _) -> do
            let t = t' + dt'
            let (dt, w') = case IM.lookup ctx tm' of
                             Nothing       -> (0, w0)
                             Just (lt, w') -> (t - lt, w')
            (mx, w) <- dt `seq` toGen w' (ws { wsDTime = dt }) inp
            let tm = IM.insert ctx (t, w) tm'
            return (mx, context' tm t)


-- | Simplified variant of 'contextInt'.  Takes a context signal only.

contextInt_ :: Monad m => Wire m Int b -> Wire m Int b
contextInt_ w0 = arr (, ()) >>> contextInt (arr fst >>> w0)


-- | Same as 'context', but with a time limit.  The first signal
-- specifies a threshold and the second signal specifies a maximum age.
-- If the current number of contexts exceeds the threshold, then all
-- contexts exceeding the maximum age are deleted.
--
-- Inherits inhibition and feedback behaviour from the current context's
-- wire.

contextLimited :: forall a b ctx m. (Ord ctx, Monad m) => Wire m (ctx, a) b -> Wire m (Int, Time, ctx, a) b
contextLimited w0 = context' M.empty 0
    where
    context' :: Map ctx (Time, Wire m (ctx, a) b) -> Time -> Wire m (Int, Time, ctx, a) b
    context' tm'' t' =
        mkGen $ \ws@(wsDTime -> dt') (limit, maxAge, ctx, x') -> do
            let t = t' + dt'
            let (dt, w') = case M.lookup ctx tm'' of
                             Nothing       -> (0, w0)
                             Just (lt, w') -> (t - lt, w')
            (mx, w) <- dt `seq` toGen w' (ws { wsDTime = dt }) (ctx, x')
            let tm' = M.insert ctx (t, w) tm''
                tm = if M.size tm' <= limit
                       then tm'
                       else M.filter (\(ct, _) -> t - ct <= maxAge) tm'

            return (mx, context' tm t)


-- | Simplified variant of 'contextLimited'.  Takes a context signal
-- only.

contextLimited_ :: (Ord ctx, Monad m) => Wire m ctx b -> Wire m (Int, Time, ctx) b
contextLimited_ w0 =
    arr (\(thr, maxAge, ctx) -> (thr, maxAge, ctx, ())) >>>
    contextLimited (arr fst >>> w0)


-- | Specialized version of 'contextLimited'.  Use this one, if your
-- contexts are 'Int's and you have a lot of them.
--
-- Inherits inhibition and feedback behaviour from the current context's
-- wire.

contextLimitedInt :: forall a b m. Monad m => Wire m (Int, a) b -> Wire m (Int, Time, Int, a) b
contextLimitedInt w0 = context' IM.empty 0
    where
    context' :: IntMap (Time, Wire m (Int, a) b) -> Time -> Wire m (Int, Time, Int, a) b
    context' tm'' t' =
        mkGen $ \ws@(wsDTime -> dt') (limit, maxAge, ctx, x') -> do
            let t = t' + dt'
            let (dt, w') = case IM.lookup ctx tm'' of
                             Nothing       -> (0, w0)
                             Just (lt, w') -> (t - lt, w')
            (mx, w) <- dt `seq` toGen w' (ws { wsDTime = dt }) (ctx, x')
            let tm' = IM.insert ctx (t, w) tm''
                tm = if IM.size tm' <= limit
                       then tm'
                       else IM.filter (\(ct, _) -> t - ct <= maxAge) tm'

            return (mx, context' tm t)


-- | Simplified variant of 'contextLimitedInt'.  Takes a context signal
-- only.

contextLimitedInt_ :: Monad m => Wire m Int b -> Wire m (Int, Time, Int) b
contextLimitedInt_ w0 =
    arr (\(thr, maxAge, ctx) -> (thr, maxAge, ctx, ())) >>>
    contextLimitedInt (arr fst >>> w0)


-- | Choose a new unique identifier at every instant.
--
-- Never inhibits.  Feedback by delay.

identifier :: MonadIO m => Wire m a Int
identifier =
    mkGen $ \ws _ -> do
        let reqVar = wsReqVar ws
        req <- liftIO . atomically $ do
                   req' <- readTVar reqVar
                   let req = succ req'
                   req `seq` writeTVar reqVar (succ req')
                   return req'
        return (Right req, identifier)


-- | Wire manager, which can be manipulated during the session.  This is
-- a convenient alternative to parallel switches.
--
-- This wire manages a set of subwires, each indexed by a key.  Through
-- messages new subwires can be added and existing ones can be deleted.
--
-- Inhibits, whenever one of the managed wires inhibits.  Inherits
-- feedback behaviour from the worst managed wire.

manager :: forall a b k m. (Monad m, Ord k) => Wire m (a, MgrMsg k m a b) (Map k b)
manager = mgr M.empty
    where
    mgr :: Map k (Wire m a b) -> Wire m (a, MgrMsg k m a b) (Map k b)
    mgr wires'' =
        mkGen $ \ws (x', msg) -> do
            let wires' = processMsg msg wires''
            wires <- T.mapM (\w -> toGen w ws x') wires'
            return (T.sequenceA (fmap fst wires), mgr (fmap snd wires))

    processMsg :: MgrMsg k m a b -> Map k (Wire m a b) -> Map k (Wire m a b)
    processMsg MgrNop = id
    processMsg (MgrMulti m1 m2) = processMsg m2 . processMsg m1
    processMsg (MgrAdd k w) = M.insert k w
    processMsg (MgrDel k) = M.delete k