module Simulation.Aivika.Distributed.Optimistic.Internal.DIO
       (DIO(..),
        DIOParams(..),
        invokeDIO,
        runDIO,
        defaultDIOParams,
        terminateDIO,
        unregisterDIO,
        dioParams,
        messageChannel,
        messageInboxId,
        timeServerId,
        logDIO,
        liftDistributedUnsafe) where
import Data.Typeable
import Data.Binary
import GHC.Generics
import Control.Applicative
import Control.Monad
import Control.Monad.Trans
import Control.Exception (throw)
import Control.Monad.Catch as C
import qualified Control.Distributed.Process as DP
import Simulation.Aivika.Trans.Exception
import Simulation.Aivika.Trans.Internal.Types
import Simulation.Aivika.Distributed.Optimistic.Internal.Channel
import Simulation.Aivika.Distributed.Optimistic.Internal.Message
import Simulation.Aivika.Distributed.Optimistic.Internal.TimeServer
import Simulation.Aivika.Distributed.Optimistic.Internal.Priority
data DIOParams =
  DIOParams { dioLoggingPriority :: Priority,
              
              dioUndoableLogSizeThreshold :: Int,
              
              dioOutputMessageQueueSizeThreshold :: Int,
              
              dioSyncTimeout :: Int,
              
              dioAllowPrematureIO :: Bool,
              
              dioAllowProcessingOutdatedMessage :: Bool
              
            } deriving (Eq, Ord, Show, Typeable, Generic)
instance Binary DIOParams
newtype DIO a = DIO { unDIO :: DIOContext -> DP.Process a
                      
                    }
data DIOContext =
  DIOContext { dioChannel :: Channel LocalProcessMessage,
               
               dioInboxId :: DP.ProcessId,
               
               dioTimeServerId :: DP.ProcessId,
               
               dioParams0 :: DIOParams
               
             }
instance Monad DIO where
  
  return = DIO . const . return
  
  (DIO m) >>= k = DIO $ \ps ->
    m ps >>= \a ->
    let m' = unDIO (k a) in m' ps
instance Applicative DIO where
  
  pure = return
  
  (<*>) = ap
instance Functor DIO where
  
  fmap f (DIO m) = DIO $ fmap f . m 
instance MonadException DIO where
  catchComp (DIO m) h = DIO $ \ps ->
    C.catch (m ps) (\e -> unDIO (h e) ps)
  finallyComp (DIO m1) (DIO m2) = DIO $ \ps ->
    C.finally (m1 ps) (m2 ps)
  
  throwComp e = DIO $ \ps ->
    throw e
invokeDIO :: DIOContext -> DIO a -> DP.Process a
invokeDIO ps (DIO m) = m ps
liftDistributedUnsafe :: DP.Process a -> DIO a
liftDistributedUnsafe = DIO . const
defaultDIOParams :: DIOParams
defaultDIOParams =
  DIOParams { dioLoggingPriority = WARNING,
              dioUndoableLogSizeThreshold = 500000,
              dioOutputMessageQueueSizeThreshold = 10000,
              dioSyncTimeout = 5000000,
              dioAllowPrematureIO = False,
              dioAllowProcessingOutdatedMessage = False
            }
dioParams :: DIO DIOParams
dioParams = DIO $ return . dioParams0
messageChannel :: DIO (Channel LocalProcessMessage)
messageChannel = DIO $ return . dioChannel
messageInboxId :: DIO DP.ProcessId
messageInboxId = DIO $ return . dioInboxId
timeServerId :: DIO DP.ProcessId
timeServerId = DIO $ return . dioTimeServerId
terminateDIO :: DIO ()
terminateDIO =
  do logDIO INFO "Terminating the simulation..."
     sender   <- messageInboxId
     receiver <- timeServerId
     liftDistributedUnsafe $
       DP.send receiver (TerminateTimeServerMessage sender)
unregisterDIO :: DIO ()
unregisterDIO =
  do logDIO INFO "Unregistering the simulation process..."
     sender   <- messageInboxId
     receiver <- timeServerId
     liftDistributedUnsafe $
       DP.send receiver (UnregisterLocalProcessMessage sender)
runDIO :: DIO a -> DIOParams -> DP.ProcessId -> DP.Process (DP.ProcessId, DP.Process a)
runDIO m ps serverId =
  do ch <- liftIO newChannel
     inboxId <-
       DP.spawnLocal $
       forever $
       do m <- DP.expect :: DP.Process LocalProcessMessage
          liftIO $
            writeChannel ch m
          when (m == TerminateLocalProcessMessage) $
            do 
               logProcess ps INFO "Terminating the inbox process..."
               
               DP.terminate
     
     logProcess ps INFO "Registering the simulation process..."
     
     DP.send serverId (RegisterLocalProcessMessage inboxId)
     return (inboxId, unDIO m DIOContext { dioChannel = ch,
                                           dioInboxId = inboxId,
                                           dioTimeServerId = serverId,
                                           dioParams0 = ps })
logDIO :: Priority -> String -> DIO ()
logDIO p message =
  do ps <- dioParams
     when (dioLoggingPriority ps <= p) $
       liftDistributedUnsafe $
       DP.say $
       embracePriority p ++ " " ++ message
logProcess :: DIOParams -> Priority -> String -> DP.Process ()
logProcess ps p message =
  when (dioLoggingPriority ps <= p) $
  DP.say $
  embracePriority p ++ " " ++ message