-- |
-- Module     : Simulation.Aivika.GPSS.Storage
-- Copyright  : Copyright (c) 2017, David Sorokin <david.sorokin@gmail.com>
-- License    : BSD3
-- Maintainer : David Sorokin <david.sorokin@gmail.com>
-- Stability  : experimental
-- Tested with: GHC 8.0.1
--
-- This module defines the GPSS Storage entity.
--
module Simulation.Aivika.GPSS.Storage
       (-- * Storage Type
        Storage,
        -- * Creating Storage
        newStorage,
        -- * Storage Properties
        storageCapacity,
        storageEmpty,
        storageFull,
        storageContent,
        storageContentStats,
        storageUseCount,
        storageUsedContent,
        storageUtilisationCount,
        storageUtilisationCountStats,
        storageQueueCount,
        storageQueueCountStats,
        storageTotalWaitTime,
        storageWaitTime,
        storageAverageHoldingTime,
        -- * Entering-Leaving Storage
        enterStorage,
        leaveStorage,
        leaveStorageWithinEvent,
        -- * Statistics Reset
        resetStorage,
        -- * Signals
        storageContentChanged,
        storageContentChanged_,
        storageUseCountChanged,
        storageUseCountChanged_,
        storageUsedContentChanged,
        storageUsedContentChanged_,
        storageUtilisationCountChanged,
        storageUtilisationCountChanged_,
        storageQueueCountChanged,
        storageQueueCountChanged_,
        storageWaitTimeChanged,
        storageWaitTimeChanged_,
        storageChanged_) where

import Data.IORef
import Data.Monoid
import Data.Maybe

import Control.Monad
import Control.Monad.Trans
import Control.Exception

import Simulation.Aivika.Internal.Specs
import Simulation.Aivika.Internal.Simulation
import Simulation.Aivika.Internal.Event
import Simulation.Aivika.Internal.Cont
import Simulation.Aivika.Internal.Process
import Simulation.Aivika.QueueStrategy
import Simulation.Aivika.Statistics
import Simulation.Aivika.Signal

import Simulation.Aivika.GPSS.Transact
import Simulation.Aivika.GPSS.TransactQueueStrategy

-- | Represents a GPSS Storage entity.
data Storage = 
  Storage { storageCapacity :: Int,
            -- ^ Return the storage capacity.
            storageContentRef :: IORef Int,
            storageContentStatsRef :: IORef (TimingStats Int),
            storageContentSource :: SignalSource Int,
            storageUseCountRef :: IORef Int,
            storageUseCountSource :: SignalSource Int,
            storageUsedContentRef :: IORef Int,
            storageUsedContentSource :: SignalSource Int,
            storageUtilisationCountRef :: IORef Int,
            storageUtilisationCountStatsRef :: IORef (TimingStats Int),
            storageUtilisationCountSource :: SignalSource Int,
            storageQueueCountRef :: IORef Int,
            storageQueueCountStatsRef :: IORef (TimingStats Int),
            storageQueueCountSource :: SignalSource Int,
            storageTotalWaitTimeRef :: IORef Double,
            storageWaitTimeRef :: IORef (SamplingStats Double),
            storageWaitTimeSource :: SignalSource (),
            storageDelayChain :: StrategyQueue (TransactQueueStrategy FCFS) StorageDelayedItem }

-- | Identifies an item that was delayed.
data StorageDelayedItem =
  StorageDelayedItem { delayedItemTime :: Double,
                       delayedItemDecrement :: Int,
                       delayedItemCont :: FrozenCont () }

instance Eq Storage where
  x == y = storageContentRef x == storageContentRef y  -- unique references

-- | Create a new storage by the specified capacity.
newStorage :: Int -> Event Storage
newStorage capacity =
  Event $ \p ->
  do let r = pointRun p
         t = pointTime p
     contentRef <- newIORef capacity
     contentStatsRef <- newIORef $ returnTimingStats t capacity
     contentSource <- invokeSimulation r newSignalSource
     useCountRef <- newIORef 0
     useCountSource <- invokeSimulation r newSignalSource
     usedContentRef <- newIORef 0
     usedContentSource <- invokeSimulation r newSignalSource
     utilCountRef <- newIORef 0
     utilCountStatsRef <- newIORef $ returnTimingStats t 0
     utilCountSource <- invokeSimulation r newSignalSource
     queueCountRef <- newIORef 0
     queueCountStatsRef <- newIORef $ returnTimingStats t 0
     queueCountSource <- invokeSimulation r newSignalSource
     totalWaitTimeRef <- newIORef 0
     waitTimeRef <- newIORef emptySamplingStats
     waitTimeSource <- invokeSimulation r newSignalSource
     delayChain <- invokeSimulation r $ newStrategyQueue (TransactQueueStrategy FCFS)
     return Storage { storageCapacity = capacity,
                      storageContentRef = contentRef,
                      storageContentStatsRef = contentStatsRef,
                      storageContentSource = contentSource,
                      storageUseCountRef = useCountRef,
                      storageUseCountSource = useCountSource,
                      storageUsedContentRef = usedContentRef,
                      storageUsedContentSource = usedContentSource,
                      storageUtilisationCountRef = utilCountRef,
                      storageUtilisationCountStatsRef = utilCountStatsRef,
                      storageUtilisationCountSource = utilCountSource,
                      storageQueueCountRef = queueCountRef,
                      storageQueueCountStatsRef = queueCountStatsRef,
                      storageQueueCountSource = queueCountSource,
                      storageTotalWaitTimeRef = totalWaitTimeRef,
                      storageWaitTimeRef = waitTimeRef,
                      storageWaitTimeSource = waitTimeSource,
                      storageDelayChain = delayChain }

-- | Whether the storage is empty, i.e. completely unused.
storageEmpty :: Storage -> Event Bool
storageEmpty r =
  Event $ \p ->
  do n <- readIORef (storageContentRef r)
     return (n == storageCapacity r)

-- | Whether the storage is full, i.e. completely used.
storageFull :: Storage -> Event Bool
storageFull r =
  Event $ \p ->
  do n <- readIORef (storageContentRef r)
     return (n == 0)

-- | Return the current storage content available for use.
storageContent :: Storage -> Event Int
storageContent r =
  Event $ \p -> readIORef (storageContentRef r)

-- | Return the statistics of the storage content available for use.
storageContentStats :: Storage -> Event (TimingStats Int)
storageContentStats r =
  Event $ \p -> readIORef (storageContentStatsRef r)

-- | Signal triggered when the 'storageContent' property changes.
storageContentChanged :: Storage -> Signal Int
storageContentChanged r =
  publishSignal $ storageContentSource r

-- | Signal triggered when the 'storageContent' property changes.
storageContentChanged_ :: Storage -> Signal ()
storageContentChanged_ r =
  mapSignal (const ()) $ storageContentChanged r

-- | Return the total use count of the storage.
storageUseCount :: Storage -> Event Int
storageUseCount r =
  Event $ \p -> readIORef (storageUseCountRef r)

-- | Signal triggered when the 'storageUseCount' property changes.
storageUseCountChanged :: Storage -> Signal Int
storageUseCountChanged r =
  publishSignal $ storageUseCountSource r

-- | Signal triggered when the 'storageUseCount' property changes.
storageUseCountChanged_ :: Storage -> Signal ()
storageUseCountChanged_ r =
  mapSignal (const ()) $ storageUseCountChanged r

-- | Return the total used content of the storage.
storageUsedContent :: Storage -> Event Int
storageUsedContent r =
  Event $ \p -> readIORef (storageUsedContentRef r)

-- | Signal triggered when the 'storageUsedContent' property changes.
storageUsedContentChanged :: Storage -> Signal Int
storageUsedContentChanged r =
  publishSignal $ storageUsedContentSource r

-- | Signal triggered when the 'storageUsedContent' property changes.
storageUsedContentChanged_ :: Storage -> Signal ()
storageUsedContentChanged_ r =
  mapSignal (const ()) $ storageUsedContentChanged r

-- | Return the current utilisation count of the storage.
storageUtilisationCount :: Storage -> Event Int
storageUtilisationCount r =
  Event $ \p -> readIORef (storageUtilisationCountRef r)

-- | Return the statistics for the utilisation count of the storage.
storageUtilisationCountStats :: Storage -> Event (TimingStats Int)
storageUtilisationCountStats r =
  Event $ \p -> readIORef (storageUtilisationCountStatsRef r)

-- | Signal triggered when the 'storageUtilisationCount' property changes.
storageUtilisationCountChanged :: Storage -> Signal Int
storageUtilisationCountChanged r =
  publishSignal $ storageUtilisationCountSource r

-- | Signal triggered when the 'storageUtilisationCount' property changes.
storageUtilisationCountChanged_ :: Storage -> Signal ()
storageUtilisationCountChanged_ r =
  mapSignal (const ()) $ storageUtilisationCountChanged r

-- | Return the current queue length of the storage.
storageQueueCount :: Storage -> Event Int
storageQueueCount r =
  Event $ \p -> readIORef (storageQueueCountRef r)

-- | Return the statistics for the queue length of the storage.
storageQueueCountStats :: Storage -> Event (TimingStats Int)
storageQueueCountStats r =
  Event $ \p -> readIORef (storageQueueCountStatsRef r)

-- | Signal triggered when the 'storageQueueCount' property changes.
storageQueueCountChanged :: Storage -> Signal Int
storageQueueCountChanged r =
  publishSignal $ storageQueueCountSource r

-- | Signal triggered when the 'storageQueueCount' property changes.
storageQueueCountChanged_ :: Storage -> Signal ()
storageQueueCountChanged_ r =
  mapSignal (const ()) $ storageQueueCountChanged r

-- | Return the total wait time of the storage.
storageTotalWaitTime :: Storage -> Event Double
storageTotalWaitTime r =
  Event $ \p -> readIORef (storageTotalWaitTimeRef r)

-- | Return the statistics for the wait time of the storage.
storageWaitTime :: Storage -> Event (SamplingStats Double)
storageWaitTime r =
  Event $ \p -> readIORef (storageWaitTimeRef r)

-- | Signal triggered when the 'storageTotalWaitTime' and 'storageWaitTime' properties change.
storageWaitTimeChanged :: Storage -> Signal (SamplingStats Double)
storageWaitTimeChanged r =
  mapSignalM (\() -> storageWaitTime r) $ storageWaitTimeChanged_ r

-- | Signal triggered when the 'storageTotalWaitTime' and 'storageWaitTime' properties change.
storageWaitTimeChanged_ :: Storage -> Signal ()
storageWaitTimeChanged_ r =
  publishSignal $ storageWaitTimeSource r

-- | Return the average holding time per unit.
storageAverageHoldingTime :: Storage -> Event Double
storageAverageHoldingTime r =
  Event $ \p ->
  do s <- readIORef (storageUtilisationCountStatsRef r)
     n <- readIORef (storageUtilisationCountRef r)
     m <- readIORef (storageUsedContentRef r)
     let t  = pointTime p
         s' = addTimingStats t n s
         k  = timingStatsSum s' / (fromRational $ toRational m)
     return k

-- | Enter the storage.
enterStorage :: Storage
                -- ^ the requested storage
                -> Transact a
                -- ^ a transact that makes the request
                -> Int
                -- ^ the content decrement
                -> Process ()
enterStorage r transact decrement =
  Process $ \pid ->
  Cont $ \c ->
  Event $ \p ->
  do let t = pointTime p
     f <- invokeEvent p $ strategyQueueNull (storageDelayChain r)
     if f
       then invokeEvent p $
            invokeCont c $
            invokeProcess pid $
            enterStorage' r transact decrement
       else do c <- invokeEvent p $
                    freezeContReentering c () $
                    invokeCont c $
                    invokeProcess pid $
                    enterStorage r transact decrement
               invokeEvent p $
                 strategyEnqueueWithPriority
                 (storageDelayChain r)
                 (transactPriority transact)
                 (StorageDelayedItem t decrement c)
               invokeEvent p $ updateStorageQueueCount r 1
               
-- | Enter the storage.
enterStorage' :: Storage
                 -- ^ the requested storage
                 -> Transact a
                 -- ^ a transact that makes the request
                 -> Int
                 -- ^ the content decrement
                 -> Process ()
enterStorage' r transact decrement =
  Process $ \pid ->
  Cont $ \c ->
  Event $ \p ->
  do let t = pointTime p
     a <- readIORef (storageContentRef r)
     if a < decrement
       then do c <- invokeEvent p $
                    freezeContReentering c () $
                    invokeCont c $
                    invokeProcess pid $
                    enterStorage r transact decrement
               invokeEvent p $
                 strategyEnqueueWithPriority
                 (storageDelayChain r)
                 (transactPriority transact)
                 (StorageDelayedItem t decrement c)
               invokeEvent p $ updateStorageQueueCount r 1
       else do invokeEvent p $ updateStorageWaitTime r 0
               invokeEvent p $ updateStorageContent r (- decrement)
               invokeEvent p $ updateStorageUseCount r 1
               invokeEvent p $ updateStorageUsedContent r decrement
               invokeEvent p $ updateStorageUtilisationCount r decrement
               invokeEvent p $ resumeCont c ()

-- | Leave the storage.
leaveStorage :: Storage
                -- ^ the storage to leave
                -> Int
                -- ^ the content increment
                -> Process ()
leaveStorage r increment =
  Process $ \_ ->
  Cont $ \c ->
  Event $ \p ->
  do invokeEvent p $ leaveStorageWithinEvent r increment
     invokeEvent p $ resumeCont c ()

-- | Leave the storage.
leaveStorageWithinEvent :: Storage
                           -- ^ the storage to leave
                           -> Int
                           -- ^ the content increment
                           -> Event ()
leaveStorageWithinEvent r increment =
  Event $ \p ->
  do let t = pointTime p
     invokeEvent p $ updateStorageUtilisationCount r (- increment)
     invokeEvent p $ updateStorageContent r increment
     invokeEvent p $ enqueueEvent t $ tryEnterStorage r

-- | Try to enter the storage.
tryEnterStorage :: Storage -> Event ()
tryEnterStorage r =
  Event $ \p ->
  do let t = pointTime p
     a <- readIORef (storageContentRef r)
     if a > 0
       then invokeEvent p $ letEnterStorage r
       else return ()

-- | Let enter the storage.
letEnterStorage :: Storage -> Event ()
letEnterStorage r =
  Event $ \p ->
  do let t = pointTime p
     a <- readIORef (storageContentRef r)
     when (a > storageCapacity r) $
       throwIO $
       SimulationRetry $
       "The storage content cannot exceed the limited capacity: leaveStorage'"
     x <- invokeEvent p $
          strategyQueueDeleteBy
          (storageDelayChain r)
          (\i -> delayedItemDecrement i <= a)
     case x of
       Nothing -> return ()
       Just (StorageDelayedItem t0 decrement0 c0) ->
         do invokeEvent p $ updateStorageQueueCount r (-1)
            c <- invokeEvent p $ unfreezeCont c0
            case c of
              Nothing ->
                invokeEvent p $ letEnterStorage r
              Just c ->
                do invokeEvent p $ updateStorageContent r (- decrement0)
                   invokeEvent p $ updateStorageWaitTime r (t - t0)
                   invokeEvent p $ updateStorageUtilisationCount r decrement0
                   invokeEvent p $ updateStorageUseCount r 1
                   invokeEvent p $ updateStorageUsedContent r decrement0
                   invokeEvent p $ enqueueEvent t $ reenterCont c ()

-- | Signal triggered when one of the storage counters changes.
storageChanged_ :: Storage -> Signal ()
storageChanged_ r =
  storageContentChanged_ r <>
  storageUsedContentChanged_ r <>
  storageUtilisationCountChanged_ r <>
  storageQueueCountChanged_ r

-- | Update the storage content and its statistics.
updateStorageContent :: Storage -> Int -> Event ()
updateStorageContent r delta =
  Event $ \p ->
  do a <- readIORef (storageContentRef r)
     let a' = a + delta
     a' `seq` writeIORef (storageContentRef r) a'
     modifyIORef' (storageContentStatsRef r) $
       addTimingStats (pointTime p) a'
     invokeEvent p $
       triggerSignal (storageContentSource r) a'

-- | Update the storage use count.
updateStorageUseCount :: Storage -> Int -> Event ()
updateStorageUseCount r delta =
  Event $ \p ->
  do a <- readIORef (storageUseCountRef r)
     let a' = a + delta
     a' `seq` writeIORef (storageUseCountRef r) a'
     invokeEvent p $
       triggerSignal (storageUseCountSource r) a'

-- | Update the storage used content.
updateStorageUsedContent :: Storage -> Int -> Event ()
updateStorageUsedContent r delta =
  Event $ \p ->
  do a <- readIORef (storageUsedContentRef r)
     let a' = a + delta
     a' `seq` writeIORef (storageUsedContentRef r) a'
     invokeEvent p $
       triggerSignal (storageUsedContentSource r) a'

-- | Update the storage queue length and its statistics.
updateStorageQueueCount :: Storage -> Int -> Event ()
updateStorageQueueCount r delta =
  Event $ \p ->
  do a <- readIORef (storageQueueCountRef r)
     let a' = a + delta
     a' `seq` writeIORef (storageQueueCountRef r) a'
     modifyIORef' (storageQueueCountStatsRef r) $
       addTimingStats (pointTime p) a'
     invokeEvent p $
       triggerSignal (storageQueueCountSource r) a'

-- | Update the storage utilisation count and its statistics.
updateStorageUtilisationCount :: Storage -> Int -> Event ()
updateStorageUtilisationCount r delta =
  Event $ \p ->
  do a <- readIORef (storageUtilisationCountRef r)
     let a' = a + delta
     a' `seq` writeIORef (storageUtilisationCountRef r) a'
     modifyIORef' (storageUtilisationCountStatsRef r) $
       addTimingStats (pointTime p) a'
     invokeEvent p $
       triggerSignal (storageUtilisationCountSource r) a'

-- | Update the storage wait time and its statistics.
updateStorageWaitTime :: Storage -> Double -> Event ()
updateStorageWaitTime r delta =
  Event $ \p ->
  do a <- readIORef (storageTotalWaitTimeRef r)
     let a' = a + delta
     a' `seq` writeIORef (storageTotalWaitTimeRef r) a'
     modifyIORef' (storageWaitTimeRef r) $
       addSamplingStats delta
     invokeEvent p $
       triggerSignal (storageWaitTimeSource r) ()

-- | Reset the statistics.
resetStorage :: Storage -> Event ()
resetStorage r =
  Event $ \p ->
  do let t = pointTime p
     content <- readIORef (storageContentRef r)
     writeIORef (storageContentStatsRef r) $
       returnTimingStats t content
     writeIORef (storageUseCountRef r) 0
     let usedContent = storageCapacity r - content
     writeIORef (storageUsedContentRef r) usedContent
     utilCount <- readIORef (storageUtilisationCountRef r)
     writeIORef (storageUtilisationCountStatsRef r) $
       returnTimingStats t utilCount
     queueCount <- readIORef (storageQueueCountRef r)
     writeIORef (storageQueueCountStatsRef r) $
       returnTimingStats t queueCount
     writeIORef (storageTotalWaitTimeRef r) 0
     writeIORef (storageWaitTimeRef r) emptySamplingStats
     invokeEvent p $
       triggerSignal (storageUseCountSource r) 0
     invokeEvent p $
       triggerSignal (storageUsedContentSource r) usedContent
     invokeEvent p $
       triggerSignal (storageUtilisationCountSource r) utilCount
     invokeEvent p $
       triggerSignal (storageWaitTimeSource r) ()