-- |
-- Module      :  Control.Concurrent.Actor.Internal
-- Copyright   :  (c) 2014 Forkk
-- License     :  MIT
-- Maintainer  :  forkk@forkk.net
-- Stability   :  experimental
-- Portability :  GHC only (requires throwTo)
--
-- Module exposing more of hactor's internals. Use with caution.
--
module Control.Concurrent.Actor.Internal
    (
    -- * Types
      ActorHandle (..)
    , ActorMessage
    , ActorM
    -- * Sending Messages
    , send
    , sendIO
    -- * Receiving Messages
    , receive
    , receiveMaybe
    , receiveSTM
    -- * Spawning Actors
    , spawnActor
    , runActor
    -- * Getting Information
    , self
    , actorThread
    -- * Internals
    , ActorContext (..)
    , MailBox
    , getContext
    , getMailBox
    ) where

import Control.Concurrent
import Control.Concurrent.STM
import Control.Monad.Reader


-- {{{ Types

-- {{{ Message

-- | The @ActorMessage@ class must be implemented by any type that will be sent
-- as a message to actors.
-- Any given type of actor will have one @ActorMessage@ type that is sent to
-- that actor. This ensures type safety.
-- Currently this is simply a dummy class with nothing in it, but things may be
-- added in the future.
class ActorMessage msg

-- Allow actors that don't take messages.
instance ActorMessage ()

-- }}}

-- {{{ Handle and context

-- | An @ActorHandle@ acts as a reference to a specific actor.
data ActorMessage msg => ActorHandle msg = ActorHandle
    { ahContext     :: ActorContext msg     -- Context for this handle's actor.
    , ahThread      :: ThreadId             -- The actor's thread ID.
    }

-- | The @ActorContext@ holds shared information about a given actor.
-- This is information such as the actor's mail box, the list of actors it's
-- linked to, etc.
data ActorMessage msg => ActorContext msg = ActorContext
    { acMailBox :: MailBox msg  -- Channel for the actor's messages.
    }

-- | The type for the actor's mail box.
type MailBox msg = TChan msg

-- }}}

-- }}}

-- | The base actor monad.
type ActorM msg = ReaderT (ActorContext msg) IO

-- {{{ Get info

-- | Gets a handle to the current actor.
self :: ActorMessage msg => ActorM msg (ActorHandle msg)
self = do
    context <- ask
    thread <- liftIO $ myThreadId
    return $ ActorHandle context thread

-- | Retrieves the mail box for the current actor.
-- This is an internal function and may be dangerous. Use with caution.
getMailBox :: ActorMessage msg => ActorM msg (MailBox msg)
getMailBox = asks acMailBox


-- | Gets the internal context object for the current actor.
-- This is an internal function and may be dangerous. Use with caution.
getContext :: ActorMessage msg => ActorM msg (ActorContext msg)
getContext = ask

-- }}}

-- {{{ Receiving

-- | Reads a message from the actor's mail box.
-- If there are no messages, blocks until one is received. If you don't want
-- this, use @receiveMaybe@ instead.
receive :: ActorMessage msg => ActorM msg (msg)
receive = do
    chan <- getMailBox
    -- Read from the channel, retrying if there is nothing to read.
    liftIO $ atomically $ readTChan chan

-- | Reads a message from the actor's mail box.
-- If there are no messages, returns @Nothing@.
receiveMaybe :: ActorMessage msg => ActorM msg (Maybe msg)
receiveMaybe = do
    chan <- getMailBox
    liftIO $ atomically $ tryReadTChan chan

-- | An @ActorM@ action which returns an @STM@ action to receive a message.
receiveSTM :: ActorMessage msg => ActorM msg (STM msg)
receiveSTM = do
    chan <- getMailBox
    return $ readTChan chan

-- }}}

-- {{{ Sending

-- | Sends a message to the given actor handle.
-- This is secretly just @sendIO@ lifted into an actor monad.
send :: (ActorMessage msg, ActorMessage msg') =>
        ActorHandle msg -> msg -> ActorM msg' ()
send hand msg = liftIO $ sendIO hand msg

-- | Sends a message to the given actor handle from within the IO monad.
sendIO :: ActorMessage msg => ActorHandle msg -> msg -> IO ()
sendIO hand msg =
    atomically $ writeTChan mailBox $ msg
  where
    mailBox = handleMailBox hand

-- }}}

-- {{{ Spawning

-- | Internal function for starting actors.
-- This takes an @ActorM@ action, makes a channel for it, wraps it in exception
-- handling stuff, and turns it into an IO monad. The function returns a tuple
-- containing the actor's context and the IO action to execute the actor.
wrapActor :: ActorMessage msg => ActorM msg () -> IO (IO (), ActorContext msg)
wrapActor actorAction = do
    -- TODO: Exception handling.
    -- First, create a channel for the actor.
    chan <- atomically newTChan
    -- Next, create the context and run the ReaderT action.
    let context = ActorContext chan
        ioAction = runReaderT actorAction context
    -- Return the information.
    return (ioAction, context)


-- | Spawns the given actor on another thread and returns a handle to it.
spawnActor :: ActorMessage msg => ActorM msg () -> IO (ActorHandle msg)
spawnActor actorAction = do
    -- Wrap the actor action.
    (ioAction, context) <- wrapActor actorAction
    -- Fork the actor's IO action to another thread.
    thread <- forkIO ioAction
    -- Return the handle.
    return $ ActorHandle context thread

-- | Runs the given actor on the current thread.
-- This function effectively turns the current thread into the actor's thread.
-- Obviously, this means that this function will block until the actor exits.
-- You probably want to use this for your "main" actor.
runActor :: ActorMessage msg => ActorM msg () -> IO ()
runActor actorAction = do
    -- Wrap the actor action. We discard the context, because we won't be
    -- returning a handle to this actor.
    (ioAction, _) <- wrapActor actorAction
    -- Execute the IO action on the current thread.
    ioAction

-- }}}

-- {{{ Utility functions

-- | Gets the mail box for the given handle.
handleMailBox :: ActorMessage msg => ActorHandle msg -> MailBox msg
handleMailBox = acMailBox . ahContext

-- | Gets the thread ID for the given actor handle.
actorThread :: ActorMessage msg => ActorHandle msg -> ThreadId
actorThread = ahThread

-- }}}