module Control.Concurrent.Priority.RoomCore
    (Room,
     newRoom,
     Claim,
     ClaimMode(..),
     claimedRoom,
     claimedThread,
     userData,
     approve,
     claim_,
     inUse)
    where

import Data.Unique
import Data.Set as Set
import Data.Map as Map
import GHC.Conc
import Control.Monad
import Control.Exception

-- | A resource pool, parameterized against arbitrary user data.
data Room u = Room (u,Unique) (TVar (Set ThreadId))

-- | A 'Claim', or attempt to acquire or release a 'Room'.
data Claim u = Claim (Room u) ThreadId ClaimMode (TVar Bool)

data ClaimMode = Acquire | Release deriving (Eq)

instance Eq (Room u) where
    (==) (Room u1 _) (Room u2 _) = snd u1 == snd u2

instance Ord (Room u) where
    compare (Room u1 _) (Room u2 _) = compare (snd u1) (snd u2)

-- | Create a new Room with some arbitrary user data.
newRoom :: u -> IO (Room u)
newRoom u = return Room `ap` liftM ((,) u) newUnique `ap` atomically (newTVar Set.empty)

-- | Get the user data associated with a 'Room'.
userData :: Room u -> u
userData (Room (u,_) _) = u

-- | Whether a Claim is to acquire or release a room.
claimMode :: Claim u -> ClaimMode
claimMode (Claim _ _ b _) = b

-- | Get the 'Room' target of a 'Claim'.
claimedRoom :: Claim u -> Room u
claimedRoom (Claim m _ _ _) = m

-- | Get the thread attempting a 'Claim'.
claimedThread :: Claim u -> ThreadId
claimedThread (Claim _ t _ _) = t

-- | Approve a claim.  This actually acquires a 'Room'.
approve :: Claim u -> STM ()
approve (Claim (Room _ m) me want claim_var) =
    do claim_state <- readTVar claim_var
       when (claim_state == False) $ 
           do writeTVar claim_var True
              writeTVar m . (case want of Acquire -> Set.insert; Release -> Set.delete) me =<< readTVar m

-- | Acquire and/or release some rooms for the duration of a critical section.
--
-- * which 'Room's to 'Acquire', and later release, or 'Release' and later reacquire for the duration of the critical section.
--
-- * a transaction to 'approve' all entering 'Claim's
--
-- * a transaction to 'approve' all exiting 'Claim's
--
-- * a transaction to run one or more times if and only if this thread is waiting for approval
--
-- * the critical section
--
-- A separate 'Claim' is generated each time a Room needs to be acquired.  The critical
-- section will not enter until every claim has been 'approve'd.
--
-- When the critical section exits, an inverse group of claims will be generated, and the critical
-- section will not exit until those claims have been 'approve'd.
--
-- It is guaranteed that when and only when all 'Claim's have been 'approve'd, the waiting thread will enter
-- (or exit) the critical section.  The lock on each 'Room' is acquired when it's 'Claim' is 'approve'd,
-- not when the critical section is entered.
--
-- 'Claim's may be 'approve'd from any transaction, even from another thread.
--
claim_ :: Map (Room u) ClaimMode -> ([Claim u] -> STM ()) -> ([Claim u] -> STM ()) -> STM () -> IO a -> IO a
claim_ entering_rooms_map approveEnteringSTM approveExitingSTM waitingSTM actionIO =
    do let entering_rooms = Map.toList entering_rooms_map
       me <- myThreadId
       -- transition: generate and request (but do not wait for) approval for all claims
       let transition rooms approveSTM = atomically $
               (\claims -> approveSTM (Prelude.filter ((== Acquire) . claimMode) claims) >> return claims) =<< -- request approval of Acquire claims.
                   (mapM $ \c -> when (claimMode c == Release) (approve c) >> return c) =<< --auto-approve Release claims.
                   (mapM $ \(m,want) -> liftM (Claim m me want) (newTVar False)) =<< -- build claims
                   filterM (\(m,want) -> liftM ((/= (want == Acquire)) . Set.member me) $ inUse m) rooms -- get the difference between the rooms we want and the rooms we have
       -- confirm: wait for all claims to be approved
       let confirm claims = forM_ claims $ \(Claim _ _ _ claim_var) ->
               do claim_state <- readTVar claim_var
                  unless claim_state retry
       -- confirm or perform the user's wait action, return True iff we have confirmed
       let confirmWithWaitAction claims=
               do done <- atomically $ (confirm claims >> return True) `orElse` (waitingSTM >> return False)
                  unless done $ confirmWithWaitAction claims
       -- when entering we signal the claims (transition) and then wait on the approval of the claims (confirm)
       -- then we generate a list of inverse claims for when it comes time to exit the critical section
       -- If an exception is thrown from the approve*STM or waitingSTM, this would leave dangling 
       -- room locks, therefore, we force the rooms into their former state, ignoring any 
       -- constraints that might have been placed on them.  On the theory that it's better to violate 
       -- constraints than to leave dangling locks.
       let transitionAndConfirm approveSTM rooms = flip finally (transition rooms $ mapM_ approve) $
               do claims <- transition rooms approveSTM
                  confirmWithWaitAction claims
                  return $ Prelude.map (\(Claim m _ want _) -> (m,case want of Acquire -> Release; Release -> Acquire)) claims
       bracket (transitionAndConfirm approveEnteringSTM entering_rooms)
               (transitionAndConfirm approveExitingSTM)
               (const actionIO)

-- | Get all 'ThreadId's that are currently claimimg this 'Room'.
inUse :: Room u -> STM (Set ThreadId)
inUse (Room _ m) = readTVar m