{-# LANGUAGE DeriveFunctor, GeneralizedNewtypeDeriving,
   ExistentialQuantification, ScopedTypeVariables #-}

module Rasa.Internal.Scheduler
  ( Scheduler(..)
  , Hook
  , Hooks
  , afterRender
  , beforeEvent
  , beforeRender
  , dispatchEvent
  , eventListener
  , getHooks
  , matchingHooks
  , onExit
  , onInit
  , onRender
  ) where


import Rasa.Internal.Action
import Rasa.Internal.Events

import Control.Lens
import Control.Monad.Reader
import Control.Monad.State
import Data.Dynamic
import Data.Foldable
import Data.Map
import Unsafe.Coerce

-- | The Scheduler is how you can register your extension's actions to run
-- at different points in the editor's event cycle.
--
-- The event cycle proceeds as follows:
--
-- @
--     Init  (Runs ONCE)
--
--     -- The following loops until an exit is triggered:
--     BeforeEvent -> (any event) -> BeforeRender -> OnRender -> AfterRender
--
--     Exit (Runs ONCE)
-- @
--
-- Each extension which wishes to perform actions exports a @'Scheduler' ()@
-- which the user inserts in their config file.
newtype Scheduler a = Scheduler
  { runSched :: State Hooks a
  } deriving (Functor, Applicative, Monad, MonadState Hooks)

-- | Use this to dispatch an event of any type, any hooks which are listening for this event will be triggered
-- with the provided event. Use this within an Action.
dispatchEvent :: Typeable a => a -> Action ()
dispatchEvent evt = do
  hooks <- ask
  traverse_ ($ evt) (matchingHooks hooks)

-- | This is a helper which extracts and coerces a hook from its wrapper back into the proper event handler type.
getHook :: forall a. Hook -> (a -> Action ())
getHook = coerce
  where
    coerce :: Hook -> (a -> Action ())
    coerce (Hook x) = unsafeCoerce x

-- | This extracts all event listener hooks from a map of hooks which match the type of the provided event.
matchingHooks :: forall a. Typeable a => Hooks -> [a -> Action ()]
matchingHooks hooks = getHook <$> (hooks^.at (typeRep (Proxy :: Proxy a))._Just)

-- | This registers an event listener hook, as long as the listener is well-typed similar to this:
--
-- @MyEventType -> Action ()@ then it will be registered to listen for dispatched events of that type.
-- Use within the 'Rasa.Internal.Scheduler.Scheduler' and add have the user add it to their config.
eventListener :: forall a. Typeable a => (a -> Action ()) -> Scheduler ()
eventListener hook = modify $ insertWith mappend (typeRep (Proxy :: Proxy a)) [Hook hook]

-- | Transform a 'Rasa.Internal.Scheduler.Scheduler' monad into a 'Hooks' map.
getHooks :: Scheduler () -> Hooks
getHooks = flip execState mempty . runSched


-- | Registers an action to be performed during the Initialization phase.
--
-- This phase occurs exactly ONCE when the editor starts up.
onInit :: Action () -> Scheduler ()
onInit action = eventListener (const action :: Init -> Action ())

-- | Registers an action to be performed BEFORE each event phase.
beforeEvent :: Action () -> Scheduler ()
beforeEvent action = eventListener (const action :: BeforeEvent -> Action ())

-- | Registers an action to be performed BEFORE each render phase.
--
-- This is a good spot to add information useful to the renderer
-- since all actions have been performed. Only cosmetic changes should
-- occur during this phase.
beforeRender :: Action () -> Scheduler ()
beforeRender action = eventListener (const action :: BeforeRender -> Action ())

-- | Registers an action to be performed during each render phase.
--
-- This phase should only be used by extensions which actually render something.
onRender :: Action () -> Scheduler ()
onRender action = eventListener (const action :: OnRender -> Action ())

-- | Registers an action to be performed AFTER each render phase.
--
-- This is useful for cleaning up extension state that was registered for the
-- renderer, but needs to be cleared before the next iteration.
afterRender :: Action () -> Scheduler ()
afterRender action = eventListener (const action :: AfterRender -> Action ())

-- | Registers an action to be performed during the exit phase.
--
-- This is only triggered exactly once when the editor is shutting down. It
-- allows an opportunity to do clean-up, kill any processes you've started, or
-- save any data before the editor terminates.

onExit :: Action () -> Scheduler ()
onExit action = eventListener (const action :: Exit -> Action ())