{-|
Module      : Game.GoreAndAsh.Actor.API
Description : Monadic and arrow API for actor core module
Copyright   : (c) Anton Gushcha, 2015-2016
License     : BSD3
Maintainer  : ncrashed@gmail.com
Stability   : experimental
Portability : POSIX

Module that contains monadic and arrow API of actor module.
-}
module Game.GoreAndAsh.Actor.API(
    ActorMonad(..)
  , ActorException(..)
  -- * Message API
  , actorSend
  , actorSendMany
  , actorSendDyn
  , actorSendManyDyn
  , actorProcessMessages
  , actorProcessMessagesM
  , actorMessages
  -- * Actor API
  , makeActor
  , makeFixedActor
  , runActor
  , runActor'
  -- * Helpers for libraries
  , getActorFingerprint
  ) where

import Control.Monad.Catch
import Control.Monad.State.Strict
import Control.Wire
import Control.Wire.Unsafe.Event
import Data.Dynamic
import Data.Maybe (isJust, fromJust)
import GHC.Generics 
import Prelude hiding (id, (.))
import qualified Data.Foldable as F
import qualified Data.HashMap.Strict as H 
import qualified Data.Sequence as S 

import Game.GoreAndAsh
import Game.GoreAndAsh.Actor.Indexed
import Game.GoreAndAsh.Actor.Message
import Game.GoreAndAsh.Actor.Module
import Game.GoreAndAsh.Actor.State
import Game.GoreAndAsh.Actor.TypeRep

-- | Exceptions thrown by ActorMonad
data ActorException = 
  ActorIdConflict TypeRep Int -- ^ Tried to register already presented actor
  deriving (Show, Generic)

instance Exception ActorException

-- | Low level monadic API for module.
class MonadThrow m => ActorMonad m where 
  -- | Registers new actor in message system
  actorRegisterM :: ActorMessage i => m i 
  
  -- | Registers specific id, throws ActorException if there is id clash
  actorRegisterFixedM :: ActorMessage i => i -> m ()

  -- | Deletes actor with given id
  actorDeleteM :: ActorMessage i => i -> m ()

  -- | Checks if given id is already taken
  actorRegisteredM :: ActorMessage i => i -> m Bool

  -- | Sends typed message to actor with given id
  actorSendM :: (ActorMessage i, Typeable (ActorMessageType i)) 
    => i -> ActorMessageType i -> m ()

  -- | Get all messages that were collected for given actor's id
  --
  -- Note: Doesn't clears the queue
  actorGetMessagesM :: (ActorMessage i, Typeable (ActorMessageType i))
    => i -> m (S.Seq (ActorMessageType i))

  -- | Find type representation of actor by it type name
  findActorTypeRepM :: String -> m (Maybe HashableTypeRep)

  -- | Register type representation for actor (sometimes this should be done before
  -- any actor is registered)
  registerActorTypeRepM :: forall proxy i . ActorMessage i => proxy i -> m ()

instance {-# OVERLAPPING #-} MonadThrow m => ActorMonad (ActorT s m) where
  actorRegisterM = do 
    astate <- ActorT get 
    let (fpt, i, astate') = pushActorNextId astate 
    ActorT . put $! astate' {
        actorBoxes = H.insert (fpt, toCounter i) (S.empty, S.empty) $! actorBoxes astate'
      , actorNameMap = H.insert (show fpt) fpt $! actorNameMap astate'
      }
    return i 

  actorRegisterFixedM i = do 
    astate <- ActorT get
    case regActorFixedId i astate of 
      Nothing -> throwM $! ActorIdConflict tp (toCounter i)
        where tp = fromHashableTypeRep $ getActorFingerprint i
      Just astate' -> ActorT . put $! astate'

  actorDeleteM i = do 
    astate <- ActorT get 
    ActorT . put $! deleteActorId i astate
  
  actorRegisteredM i = do 
    astate <- ActorT get 
    return . isActorIdRegistered i $! astate

  actorSendM i msg = do 
    astate <- ActorT get 
    ActorT . put $! putActorMessage i (toDyn msg) astate

  actorGetMessagesM i = do 
    astate <- ActorT get 
    let msgs = getActorMessages i astate 
    return . catMaybesSeq . fmap fromDynamic $! msgs

  findActorTypeRepM n = do 
    astate <- ActorT get 
    return . H.lookup n . actorNameMap $! astate

  registerActorTypeRepM p = do 
    astate <- ActorT get 
    let fp = actorFingerprint p
    ActorT . put $! astate {
        actorNameMap = H.insert (show fp) fp . actorNameMap $! astate
      }

instance {-# OVERLAPPABLE #-} (MonadThrow (mt m), ActorMonad m, MonadTrans mt) => ActorMonad (mt m) where 
  actorRegisterM = lift actorRegisterM
  actorRegisterFixedM = lift . actorRegisterFixedM
  actorDeleteM = lift . actorDeleteM
  actorRegisteredM = lift . actorRegisteredM
  actorSendM a b = lift $ actorSendM a b
  actorGetMessagesM = lift . actorGetMessagesM
  findActorTypeRepM = lift . findActorTypeRepM
  registerActorTypeRepM = lift . registerActorTypeRepM

-- | Leaves only Just values  
catMaybesSeq :: S.Seq (Maybe a) -> S.Seq a 
catMaybesSeq = fmap fromJust . S.filter isJust

-- | Helper to get actor fingerprint from id value
getActorFingerprint :: forall i . ActorMessage i => i -> HashableTypeRep
getActorFingerprint _ = actorFingerprint (Proxy :: Proxy i)

-- | Returns next unregistered id of actor and updates internal state
pushActorNextId :: forall i s . ActorMessage i => ActorState s -> (HashableTypeRep, i, ActorState s)
pushActorNextId !s = case H.lookup k (actorBoxes s) of
  Just _ -> pushActorNextId nextState
  Nothing -> (fingerprint, nextId, nextState)
  where 
    fingerprint = actorFingerprint (Proxy :: Proxy i)
    (nextId, nextState) = rawGet s 
    k = (fingerprint, toCounter nextId)

    -- | Update @actorNextId@ map
    rawGet :: ActorState s -> (i, ActorState s)
    rawGet s' = case H.lookup fingerprint (actorNextId s') of 
      Nothing -> (fromCounter 0, s' {
        actorNextId = H.insert fingerprint 1 (actorNextId s')
        })
      Just i -> (fromCounter i, s' {
        actorNextId = H.insert fingerprint (i+1) (actorNextId s')
        })
 
-- | Try to register given id in the mailbox map
regActorFixedId :: forall i s . ActorMessage i => i -> ActorState s -> Maybe (ActorState s)
regActorFixedId !i !s = case H.lookup k (actorBoxes s) of 
  Just _ -> Nothing
  Nothing -> Just $! s {
      actorBoxes = H.insert k (S.empty, S.empty) . actorBoxes $! s
    , actorNameMap = H.insert (show fingerprint) fingerprint . actorNameMap $! s
    }
  where
    fingerprint = actorFingerprint (Proxy :: Proxy i)
    k = (fingerprint, toCounter i)

-- | Remove actor id from mailbox map
deleteActorId :: forall i s . ActorMessage i => i -> ActorState s -> ActorState s 
deleteActorId !i !s = s {
    actorBoxes = H.delete k . actorBoxes $! s 
  , actorNameMap = H.delete (show fingerprint) . actorNameMap $! s
  }
  where
    fingerprint = actorFingerprint (Proxy :: Proxy i)
    k = (fingerprint, toCounter i)

-- | Returns True if given ID is registered
isActorIdRegistered :: forall i s . ActorMessage i => i -> ActorState s -> Bool
isActorIdRegistered !i !s = case H.lookup k . actorBoxes $! s of 
  Nothing -> False 
  Just _ -> True 
  where
    fingerprint = actorFingerprint (Proxy :: Proxy i)
    k = (fingerprint, toCounter i)

-- | Appends given message in corresponding message queue
putActorMessage :: forall i s . ActorMessage i => i -> Dynamic -> ActorState s -> ActorState s
putActorMessage !i !msg !s = case H.lookup k . actorBoxes $! s of 
  Nothing -> s
  Just (msgsR, msgsS) -> s {
      actorBoxes = H.insert k (msgsR, msgsS S.|> msg) . actorBoxes $! s
    }
  where
    fingerprint = actorFingerprint (Proxy :: Proxy i)
    k = (fingerprint, toCounter i)

-- | Return all messages in corresponding mailbox
getActorMessages :: forall i s . ActorMessage i => i -> ActorState s -> S.Seq Dynamic
getActorMessages !i !s = case H.lookup k . actorBoxes $! s of 
  Nothing -> S.empty
  Just (msgs, _) -> msgs
  where
    fingerprint = actorFingerprint (Proxy :: Proxy i)
    k = (fingerprint, toCounter i)

-- | Sends message to statically known actor
actorSend :: (ActorMonad m, ActorMessage i, Typeable (ActorMessageType i)) 
  => i -> GameWire m (Event (ActorMessageType i)) (Event ())
actorSend i = liftGameMonadEvent1 $ actorSendM i

-- | Sends many messages to statically known actor
actorSendMany :: (ActorMonad m, ActorMessage i, Typeable (ActorMessageType i), F.Foldable t)
  => i -> GameWire m (Event (t (ActorMessageType i))) (Event ())
actorSendMany i = liftGameMonadEvent1 $ F.mapM_ (actorSendM i)

-- | Sends message to actor with incoming id
actorSendDyn :: (ActorMonad m, ActorMessage i, Typeable (ActorMessageType i)) 
  => GameWire m (Event (i, ActorMessageType i)) (Event ())
actorSendDyn = liftGameMonadEvent1 $ \(i, m) -> actorSendM i m

-- | Sends many messages, dynamic version of actorSendMany which takes actor id as arrow input
actorSendManyDyn :: (ActorMonad m, ActorMessage i, Typeable (ActorMessageType i), F.Foldable t)
  => GameWire m (Event (t (i, ActorMessageType i))) (Event ())
actorSendManyDyn = liftGameMonadEvent1 $ F.mapM_ (uncurry actorSendM)

-- | Helper to process all messages from message queue and update a state
actorProcessMessages :: (ActorMonad m, ActorMessage i, Typeable (ActorMessageType i))
  => i -- ^ Actor id known statically
  -> (a -> ActorMessageType i -> a) -- ^ Action that modifies accumulator
  -> GameWire m a a -- ^ Wire that updates input value using supplied function
actorProcessMessages i f = liftGameMonad1 $ \a -> do 
  msgs <- actorGetMessagesM i
  return . F.foldl' f a $! msgs

-- | Helper to process all messages from message queue and update a state (monadic version)
actorProcessMessagesM :: (ActorMonad m, ActorMessage i, Typeable (ActorMessageType i))
  => i -- ^ Actor id known statically
  -> (a -> ActorMessageType i -> GameMonadT m a) -- ^ Monadic action that modifies accumulator
  -> GameWire m a a -- ^ Wire that updates input value using supplied function
actorProcessMessagesM i f = liftGameMonad1 $ \a -> do 
  msgs <- actorGetMessagesM i 
  foldM f a msgs

-- | Registers new index for wire and makes an actor wire
makeActor :: (ActorMonad m, ActorMessage i) 
  => (i -> GameWire m a b) -- ^ Body wire
  -> GameActor m i a b -- ^ Operation that makes actual actor
makeActor wbody = do 
  i <- actorRegisterM
  return $! GameWireIndexed i (wbody i)

-- | Registers new actor with fixed id, can fail with ActorException if there is already 
-- registered actor for that id
makeFixedActor :: (ActorMonad m, ActorMessage i) 
  => i -- ^ Manual id of actor
  -> GameWire m a b -- ^ Body wire
  -> GameActor m i a b -- ^ Operation that makes actual actor
makeFixedActor i wbody = do 
  actorRegisterFixedM i
  return $! GameWireIndexed i wbody

-- | If need no dynamic switching, you can use the function to embed index wire just at time
runActor :: ActorMonad m 
  => GameActor m i a b -- ^ Actor creator
  -> GameWire m a (b, i) -- ^ Usual wire that also returns id of inner indexed wire
runActor actor = switch makeWire
  where
  -- | Switches immidieatly to created wire, thats why error is used for
  -- value that should be returned in case where there is no event.
  makeWire = proc _ -> do 
    e <- mapE (\iw -> arr (, indexedId iw) . indexedWire iw) . now . liftGameMonadOnce actor -< ()
    returnA -< (error "runActor: impossible", e)

-- | Same as runActor, but doesn't return id of actor
runActor' :: ActorMonad m 
  => GameActor m i a b -- ^ Actor creator
  -> GameWire m a b -- ^ Usual wire
runActor' actor = arr fst . runActor actor

-- | Non-centric style of subscribing to messages
actorMessages :: (ActorMonad m, ActorMessage i, Typeable (ActorMessageType i))
  => i -- ^ Actor id which messages we look for
  -> (ActorMessageType i -> Bool) -- ^ Filter function, leaves only with True return value
  -> GameWire m a (Event (S.Seq (ActorMessageType i)))
actorMessages i f = liftGameMonad $ do 
  msgs <- S.filter f <$> actorGetMessagesM i 
  return $! if S.null msgs then NoEvent
    else Event msgs