{-# LANGUAGE ScopedTypeVariables #-}

-- | Blockers are used to implement variable set sources which can be
-- turned on and off.  They are indexed by a BlockID.
module Util.VariableSetBlocker(
   Blocker,
   BlockID,
   newBlocker, -- :: HasKey a key => VariableSetSource a -> IO (Blocker a)
   newBlockID, -- :: IO BlockID

   openBlocker, -- :: HasKey a key => Blocker a -> BlockID -> IO ()
   closeBlocker, -- :: HasKey a key => Blocker a -> BlockID -> IO ()
   blockVariableSet,
      -- :: HasKey a key => Blocker a -> BlockID -> VariableSetSource a


   newBlockerWithPreAction
      -- :: HasKey a key => VariableSetSource a -> ([a] -> IO ())
      -- -> IO (Blocker a)
      --
      -- newBlockerWithPreAction creates a blocker that additionally permits
      -- an action that is performed the very first time the blocker is
      -- opened.
      -- The arguments to the action are the contents of the variable set
      -- at about the time of the opening.
   ) where

import System.IO.Unsafe
import Control.Concurrent

import Util.Object
import Util.Registry
import Util.Sink
import Util.Sources
import Util.VariableSet


-- --------------------------------------------------------------------
-- The types
-- --------------------------------------------------------------------

data Blocker a = Blocker {
   registry :: Registry BlockID (VariableSetSource a,Bool -> IO ()),
      -- For each blockID, the corresponding VariableSetSource and an
      -- action which blocks it, with True meaning "blocked".
   setSource :: VariableSetSource a
   }

newtype BlockID = BlockID ObjectID deriving (Eq,Ord)

-- --------------------------------------------------------------------
-- The functions
-- --------------------------------------------------------------------

newBlocker :: HasKey a key => VariableSetSource a -> IO (Blocker a)
newBlocker setSource =
   do
      registry <- newRegistry
      let
         blocker = Blocker {
            registry = registry,
            setSource = setSource
            }
      return blocker

newBlockerWithPreAction
   :: HasKey a key => VariableSetSource a -> ([a] -> IO ()) -> IO (Blocker a)
newBlockerWithPreAction setSource0 preAction =
   let
      action =
         do
            list <- readContents setSource0
            preAction list
      setSource1 = (unsafePerformIO action) `seq` setSource0
   in
      newBlocker setSource1

newBlockID :: IO BlockID
newBlockID =
   do
      objectID <- newObject
      return (BlockID objectID)

openBlocker :: HasKey a key => Blocker a -> BlockID -> IO ()
openBlocker blocker blockID =
   do
      (_,blockFn) <- getBlockEntry blocker blockID
      blockFn False

closeBlocker :: HasKey a key => Blocker a -> BlockID -> IO ()
closeBlocker blocker blockID =
   do
      (_,blockFn) <- getBlockEntry blocker blockID
      blockFn True

blockVariableSet :: HasKey a key
   => Blocker a -> BlockID -> IO (VariableSetSource a)
blockVariableSet blocker blockID =
   do
      (setSource,_) <- getBlockEntry blocker blockID
      return setSource

-- --------------------------------------------------------------------
-- The primitive functions
-- --------------------------------------------------------------------

getBlockEntry :: HasKey a key
   => Blocker a -> BlockID -> IO (VariableSetSource a,Bool -> IO ())
getBlockEntry blocker blockID =
   transformValue (registry blocker) blockID (\ entryOpt ->
      case entryOpt of
         Just entry -> return (entryOpt,entry)
         Nothing ->
            do
               entry <- blockableVariableSet (setSource blocker)
               return (Just entry,entry)
         )

-- | (setSource2,block) \<- blockableVariableSet setSource1
-- returns a setSource2 which is in one of two states.  In one state it is
-- blocked, and empty.  In the other, it is unblocked, and its contents are
-- the same as those of setSource1.  Initially it is blocked.  To switch
-- from one to the other the block function is used.  \"block True\" blocks
-- the set source; \"block False\" unblocks it.   Blocking if we are already
-- blocked, or unblocking if we are already unblocked, is harmless and does
-- nothing.
--
-- This somewhat baroque function is required for arc sets from folders.
-- I have wasted a couple of days trying to think of a more elegant way of
-- doing this ...
blockableVariableSet :: HasKey a key
   => VariableSetSource a -> IO (VariableSetSource a,Bool -> IO ())
blockableVariableSet (setSource1 :: VariableSetSource a) =
   do
      (mVar :: MVar (Maybe (IO ()))) <- newMVar Nothing
         -- If we are not blocked, contains the terminator action.
      set2 <- newEmptyVariableSet -- contains the contents of setSource2
      parallelX <- newParallelExec
         -- used to execute updates to set2.  This helps make sure they
         -- happen in the right order.
      let
         block doBlock = modifyMVar_ mVar (\ terminatorOpt ->
            do
               case (doBlock,terminatorOpt) of
                  (True,Just terminator) -> -- block
                     do
                        parallelExec parallelX (
                           do
                              terminator -- stop any more updates.
                              setVariableSet set2 [] -- empty this set.
                           )
                        return Nothing
                  (False,Nothing) -> -- unblock
                     do
                        sinkID <- newSinkID

                        let
                           doContents :: [a] -> IO ()
                           doContents contents = setVariableSet set2 contents

                           doUpdate :: VariableSetUpdate a -> IO ()
                           doUpdate update = updateSet set2 update

                        addNewSinkWithInitial setSource1 doContents doUpdate
                           sinkID parallelX
                        return (Just (invalidate sinkID))
                  _ -> return terminatorOpt
                  )

      return (toSource set2,block)