{-# LANGUAGE GADTs #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}

module Eve.Internal.Listeners
  ( HasEvents
  , dispatchEvent
  , dispatchEvent_
  , dispatchLocalEvent
  , dispatchLocalEvent_
  , dispatchEventAsync

  , addListener
  , addListener_
  , addLocalListener
  , addLocalListener_

  , removeListener
  , removeLocalListener

  , asyncEventProvider

  , afterInit
  , beforeEvent
  , beforeEvent_
  , afterEvent
  , afterEvent_
  , onExit

  , Listener
  , ListenerId
  , EventDispatcher
  ) where

import Eve.Internal.States
import Eve.Internal.Async
import Eve.Internal.Actions
import Eve.Internal.Events

import Control.Monad.State
import Control.Lens

import Data.Default
import Data.Typeable
import Data.Maybe
import qualified Data.Map as M

-- | Registers an action to be performed directly following the Initialization phase.
--
-- At this point any listeners in the initialization block have run, so you may 'dispatchEvent's here.
afterInit :: forall base m a. (Monad m, HasEvents base, Typeable m) => AppT base m a -> AppT base m ()
afterInit action = addListener_ (const (void action) :: AfterInit -> AppT base m ())

-- | Registers an action to be performed BEFORE each async event is processed phase.
beforeEvent :: forall base zoomed m a. (Monad m, HasEvents base, Typeable m) => AppT base m a -> ActionT base zoomed m ListenerId
beforeEvent action = addListener (const (void action) :: BeforeEvent -> AppT base m ())

beforeEvent_ :: (Monad m, HasEvents base, Typeable m) => AppT base m a -> ActionT base zoomed m ()
beforeEvent_ = void . beforeEvent

-- | Registers an action to be performed AFTER each event phase.
afterEvent :: forall base zoomed m a. (Monad m, HasEvents base, Typeable m) => AppT base m a -> ActionT base zoomed m ListenerId
afterEvent action = addListener (const (void action) :: AfterEvent -> AppT base m ())

afterEvent_ :: (Monad m, HasEvents base, Typeable m) => AppT base m a -> ActionT base zoomed m ()
afterEvent_ = void . afterEvent

-- | Registers an action to be run before shutdown. Any asynchronous combinators used in this block will NOT be run.
onExit :: forall base zoomed m a. (HasEvents base, Typeable m, Monad m) => AppT base m a -> ActionT base zoomed m ()
onExit action = addListener_ (const (void action) :: Exit -> AppT base m ())

-- | A local version of 'dispatchEvent'.
-- The local version dispatches the event in the context of the current 'Action',
-- If you don't know what this means, you probably want 'dispatchEvent' instead
dispatchLocalEvent
  :: forall result eventType m s.
     (MonadState s m
     ,HasEvents s
     ,Monoid result
     ,Typeable m
     ,Typeable eventType
     ,Typeable result)
  => eventType -> m result
dispatchLocalEvent evt = do
  LocalListeners _ listeners <- use localListeners
  results <-
    traverse ($ evt) (matchingListeners listeners :: [eventType -> m result])
  return (mconcat results :: result)

dispatchLocalEvent_
  :: forall eventType m s.
     (MonadState s m
     ,HasEvents s
     ,Typeable m
     ,Typeable eventType)
  => eventType -> m ()
dispatchLocalEvent_ = dispatchLocalEvent

-- | Runs any listeners registered for the provided event with the provided event;
--
-- You can also 'query' listeners and receive a ('Monoid'al) result.
--
-- > data RequestNames = GetFirstName | GetLastName
-- > provideName1, provideName2 :: RequestNames -> App [String]
-- > provideName1 GetFirstNames = return ["Bob"]
-- > provideName1 GetLastNames = return ["Smith"]
-- > provideName2 GetFirstNames = return ["Sally"]
-- > provideName2 GetLastNames = return ["Jenkins"]
-- >
-- > -- Note that if we registered an action of type 'GetFirstName -> ()' it would NOT
-- > -- be run in response to the following 'dispatchEvent', since it's type doesn't match.
-- >
-- > greetNames :: App [String]
-- > greetNames = do
-- >   addListener_ provideName1
-- >   addListener_ provideName2
-- >   firstNames <- dispatchEvent GetFirstName
-- >   lastNames <- dispatchEvent GetLastName
-- >   liftIO $ print firstNames
-- >   -- ["Bob", "Sally"]
-- >   liftIO $ print lastNames
-- >   -- ["Smith", "Jenkins"]
dispatchEvent
  :: forall result eventType m base zoomed.
     (HasEvents base
     ,Monoid result
     ,Monad m
     ,Typeable m
     ,Typeable eventType
     ,Typeable result)
    => eventType -> ActionT base zoomed m result
dispatchEvent evt = runApp $ dispatchLocalEvent evt

dispatchEvent_
  :: forall eventType m base zoomed.
     (HasEvents base
     ,Monad m
     ,Typeable m
     ,Typeable eventType)
    => eventType -> ActionT base zoomed m ()
dispatchEvent_ = dispatchEvent

-- | The local version of 'addListener'. It will register a listener within an 'Action's local event
-- context. If you don't know what this means you probably want 'addListener' instead.
addLocalListener
  :: forall result eventType m s.
     (MonadState s m
     ,HasEvents s
     ,Typeable m
     ,Typeable eventType
     ,Typeable result
     ,Monoid result)
  => (eventType -> m result) -> m ListenerId
addLocalListener lFunc = do
  LocalListeners nextListenerId listeners <- use localListeners
  let (listener, listenerId, eventType) = mkListener nextListenerId lFunc
      newListeners = M.insertWith mappend eventType [listener] listeners
  localListeners .= LocalListeners (nextListenerId + 1) newListeners
  return listenerId
  where
    mkListener
      :: forall event r.
         (Typeable event, Typeable r, Monoid r)
      => Int -> (event -> m r) -> (Listener, ListenerId, TypeRep)
    mkListener n listenerFunc =
      let list = Listener (typeOf listenerFunc) listId listenerFunc
          listId = ListenerId n (typeRep (Proxy :: Proxy event))
          prox = typeRep (Proxy :: Proxy event)
      in (list, listId, prox)

addLocalListener_
  :: forall result eventType m s.
     (MonadState s m
     ,HasEvents s
     ,Typeable m
     ,Typeable eventType
     ,Typeable result
     ,Monoid result)
  => (eventType -> m result) -> m ()
addLocalListener_ = void . addLocalListener

-- | Registers an 'Action' or 'App' to respond to an event.
--
-- For a given use: @addListener myListener@, @myListener@ might have the type @MyEvent -> App a@
-- it will register the function @myListener@ to be run in response to a @dispatchEvent (MyEvent eventInfo)@
-- and will be provided @(MyEvent eventInfo)@ as an argument.
--
-- This returns a 'ListenerId' which corresponds to the registered listener for use with 'removeListener'
addListener
  :: forall result eventType m base zoomed.
     (HasEvents base
     ,Monad m
     ,Typeable m
     ,Typeable eventType
     ,Typeable result
     ,Monoid result)
    => (eventType -> AppT base m result) -> ActionT base zoomed m ListenerId
addListener = runApp . addLocalListener

addListener_
  :: forall result eventType m base zoomed.
     (HasEvents base
     ,Monad m
     ,Typeable m
     ,Typeable eventType
     ,Typeable result
     ,Monoid result)
    => (eventType -> AppT base m result) -> ActionT base zoomed m ()
addListener_ = void . addListener

-- | The local version of 'removeListener'.
-- This removes a listener from an 'Action's event context. If you don't
-- know what this means you probably want 'removeListener' instead.
removeLocalListener
  :: (MonadState s m, HasEvents s)
  => ListenerId -> m ()
removeLocalListener listenerId@(ListenerId _ eventType) = localListeners %= remover
  where
    remover (LocalListeners nextListenerId listeners) =
      let newListeners =
            listeners & at eventType . _Just %~ filter (notMatch listenerId)
      in LocalListeners nextListenerId newListeners
    notMatch idA (Listener _ idB _) = idA /= idB

-- | Unregisters a listener referred to by the provided 'ListenerId'
removeListener
  :: (HasEvents base
     ,Monad m)
    => ListenerId -> ActionT base zoomed m ()
removeListener = runApp . removeLocalListener

-- | This function takes an IO which results in some event, it runs the IO
-- asynchronously, THEN dispatches the event. Note that only the
-- code which generates the event is asynchronous, not any responses to the event
-- itself.
dispatchEventAsync
  :: (Typeable event
     ,MonadIO m
     ,Typeable m
     ,HasEvents base
     ) => IO event -> ActionT base zoomed m ()
dispatchEventAsync ioEvent = dispatchActionAsync $ dispatchEvent <$> ioEvent

-- | A wrapper around event listeners so they can be stored in 'Listeners'.
data Listener where
        Listener ::
            (MonadState s m, Typeable m, Typeable eventType, Typeable result,
             Monoid result, HasStates s) =>
            TypeRep -> ListenerId -> (eventType -> m result) -> Listener

-- | An opaque reverence to a specific registered event-listener.
-- A ListenerId is used only to remove listeners later with 'removeListener'.
data ListenerId =
  ListenerId Int
             TypeRep

instance Eq ListenerId where
  ListenerId a _ == ListenerId b _ = a == b

-- | A map of event types to a list of listeners for that event
type Listeners = M.Map TypeRep [Listener]

-- | Store the listeners in the state-map
data LocalListeners =
  LocalListeners Int
                 Listeners

instance Default LocalListeners where
  def = LocalListeners 0 M.empty

localListeners
  :: HasStates s
  => Lens' s LocalListeners
localListeners = stateLens

-- | This extracts all event listeners from a map of listeners which match the type of the provided event.
matchingListeners
  :: forall m eventType result.
     (Typeable m
     ,Typeable eventType
     ,Typeable result)
  => Listeners -> [eventType -> m result]
matchingListeners listeners =
  catMaybes $ (getListener :: Listener -> Maybe (eventType -> m result)) <$>
  (listeners ^. at (typeRep (Proxy :: Proxy eventType)) . _Just)

-- | Extract the listener function from eventType listener
getListener
  :: Typeable expected
  => Listener -> Maybe expected
getListener (Listener _ _ x) = cast x

-- | This is a type alias to make defining your functions for use with 'asyncEventProvider' easier;
-- It represents the function your event provider function will be passed to allow dispatching
-- events. Using this type requires the @RankNTypes@ language pragma.
type EventDispatcher = forall event. Typeable event =>
                                event -> IO ()

-- | This allows long-running IO processes to provide Events to the application asyncronously.
--
-- Don't let the type signature confuse you; it's much simpler than it seems.
--
-- Let's break it down:
--
-- Using the 'EventDispatcher' type with asyncEventProvider requires the @RankNTypes@ language pragma.
--
-- This type as a whole represents a function which accepts an 'EventDispatcher' and returns an 'IO';
-- the dispatcher itself accepts data of ANY 'Typeable' type and emits it as an event.
--
-- When you call 'asyncEventProvider' you pass it a function which accepts a @dispatch@ function as an argument
-- and then calls it with various events within the resulting 'IO'.
--
-- Note that this function calls forkIO internally, so there's no need to do that yourself.
--
-- Here's an example which fires a @Timer@ event every second.
--
-- > {-# language RankNTypes #-}
-- > data Timer = Timer
-- > myTimer :: EventDispatcher -> IO ()
-- > myTimer dispatch = forever $ dispatch Timer >> threadDelay 1000000
-- >
-- > myInit :: App ()
-- > myInit = asyncEventProvider myTimer
asyncEventProvider
  :: (HasEvents base, MonadIO m, Typeable m) => (EventDispatcher -> IO ()) -> ActionT base zoomed m ()
asyncEventProvider asyncEventProv = asyncActionProvider $ eventsToActions asyncEventProv
  where
    eventsToActions :: (Monad m, HasEvents base, Typeable m) => (EventDispatcher -> IO ()) -> (AppT base m () -> IO ()) -> IO ()
    eventsToActions aEventProv dispatcher =
      aEventProv (dispatcher . dispatchEvent)