{-# LANGUAGE RecordWildCards #-}

-- |
-- Module:
--   Reflex.Test.SimpleHost
-- Description:
--   This module contains reflex host methods for testing without external events

module Reflex.Test.SimpleHost
  ( TestGuestConstraints
  , TestGuestT
  , AppIn(..)
  , AppOut(..)
  , AppFrame(..)
  , getAppFrame
  , tickAppFrame
  , runAppSimple
  , runApp
  , runApp'
  , runAppB
  )
where

import           Prelude

import           Control.Monad
import           Control.Monad.Fix
import           Control.Monad.IO.Class
import           Control.Monad.Ref
import           Data.Dependent.Sum
import           Data.Functor.Identity
import           Data.Kind
import           Data.These

import           Reflex
import           Reflex.Host.Class


type TestGuestT t (m :: Type -> Type) = PostBuildT t (PerformEventT t m)

-- TODO some of these constraints can be dropped probably
type TestGuestConstraints t (m :: Type -> Type)
  = ( MonadReflexHost t m
    , MonadHold t m
    , MonadSample t m
    , Ref m ~ Ref IO
    , MonadRef m
    , MonadRef (HostFrame t)
    , Ref (HostFrame t) ~ Ref IO
    , MonadIO (HostFrame t)
  --, PrimMonad (HostFrame t)
    , MonadIO m
    , MonadFix m
    )

data AppIn t b e = AppIn
    { _appIn_behavior :: Behavior t b
    , _appIn_event    :: Event t e
    }

data AppOut t b e = AppOut
    { _appOut_behavior :: Behavior t b
    , _appOut_event    :: Event t e
    }

data AppFrame t bIn eIn bOut eOut m = AppFrame
    { _appFrame_readPhase :: ReadPhase m (bOut, Maybe eOut)
    , _appFrame_mpulseB :: Maybe (EventTrigger t bIn)
    , _appFrame_mpulseE :: Maybe (EventTrigger t eIn)
    , _appFrame_fire :: forall a .
  [DSum (EventTrigger t) Identity] -> ReadPhase m a -> m [a]
    }

-- | make an 'AppFrame' that takes an input behavior and event and returns an
-- output behavior and event. This will also fire the 'PostBuild' event if there
-- are any subscribers.
getAppFrame
  :: forall t bIn eIn bOut eOut m
   . (TestGuestConstraints t m)
  => (AppIn t bIn eIn -> TestGuestT t m (AppOut t bOut eOut))
  -> bIn
  -> m (AppFrame t bIn eIn bOut eOut m)
getAppFrame app b0 = do

  -- Create the "post-build" event and associated trigger. This event fires
  -- once, when the application starts.
  (postBuild , postBuildTriggerRef ) <- newEventWithTriggerRef


  -- Create input behavior, events, and  assosciated triggers.
  (appInHoldE, pulseHoldTriggerRef ) <- newEventWithTriggerRef
  (appInE    , pulseEventTriggerRef) <- newEventWithTriggerRef
  appInB                             <- hold b0 appInHoldE

  -- Setup the app and obtain its output events and 'FireCommand'
  (out :: AppOut t bOut eOut, FireCommand fire) <-
    hostPerformEventT $ flip runPostBuildT postBuild $ app $ AppIn
      { _appIn_event    = appInE
      , _appIn_behavior = appInB
      }


  -- Read the trigger reference for the post-build event. This will be
  -- 'Nothing' if the guest application hasn't subscribed to this event.
  mPostBuildTrigger <- readRef postBuildTriggerRef

  -- When there is a subscriber to the post-build event, fire the event.
  forM_ mPostBuildTrigger
    $ \postBuildTrigger -> fire [postBuildTrigger :=> Identity ()] $ return ()

  --
  hnd :: EventHandle t eOut <- subscribeEvent (_appOut_event out)
  mpulseB                   <- readRef pulseHoldTriggerRef
  mpulseE                   <- readRef pulseEventTriggerRef
  let readPhase = do
        b      <- sample (_appOut_behavior out)
        frames <- sequence =<< readEvent hnd
        return (b, frames)
  return AppFrame { _appFrame_readPhase = readPhase
                  , _appFrame_mpulseB   = mpulseB
                  , _appFrame_mpulseE   = mpulseE
                  , _appFrame_fire      = fire
                  }

-- | Tick an app frame once with optional input behavior and event values.
-- Returns behaviors and events from the app's output for each frame that run
-- for the input (i.e. 'runWithAdjust' and 'performEvent' may cause several
-- frames to run for each input)
--
-- N.B. output behavior will not reflect changes that happen during its frame
-- i.e. this is analogous to 'tag' and 'tagPromptlyDyn'. If you need the most
-- recent behavior value you can always call 'tickAppFrame' with 'Nothing' as
-- input
tickAppFrame
  :: AppFrame t bIn eIn bOut eOut m
  -> Maybe (These bIn eIn)
  -> m [(bOut, Maybe eOut)]
tickAppFrame AppFrame {..} input = r where
  fire      = _appFrame_fire
  readPhase = _appFrame_readPhase
  mpulseB   = _appFrame_mpulseB
  mpulseE   = _appFrame_mpulseE
  makeFiring mpulse v = case mpulse of
    Just pulse -> [pulse :=> Identity v]
    Nothing    -> []
  firings =    case input of
    Nothing -> []
    Just i  -> case i of
      This b'     -> makeFiring mpulseB b'
      That e'        -> makeFiring mpulseE e'
      These b'       e' -> makeFiring mpulseB b' <> makeFiring mpulseE e'
  r = fire firings readPhase


-- | calls 'tickAppFrame' for each input in a list and returns collected results
-- see comments for 'tickAppFrame'
runApp
  :: (t ~ SpiderTimeline Global, m ~ SpiderHost Global)
  => (AppIn t bIn eIn -> TestGuestT t m (AppOut t bOut eOut))
  -> bIn
  -> [Maybe (These bIn eIn)]
  -> IO [[(bOut, Maybe eOut)]]
runApp app b0 input = runSpiderHost $ do
  appFrame <- getAppFrame app b0
  forM input $ tickAppFrame appFrame

-- | run an app with provided list of input events returns list of results for
-- each input. Each result is a list of events from the app's output for each
-- frame that run for the input.
-- see comments for 'tickAppFrame'
runAppSimple
  :: (t ~ SpiderTimeline Global, m ~ SpiderHost Global)
  => (Event t eIn -> TestGuestT t m (Event t eOut))
  -> [eIn]
  -> IO [[Maybe eOut]]
runAppSimple app input = runApp' app (map Just input)

-- | same as runAppSimple except input event for each frame is optional
-- see comments for 'tickAppFrame'
runApp'
  :: (t ~ SpiderTimeline Global, m ~ SpiderHost Global)
  => (Event t eIn -> TestGuestT t m (Event t eOut))
  -> [Maybe eIn]
  -> IO [[Maybe eOut]]
runApp' app input = do
  let app' = fmap (AppOut (pure ())) . app
  map (map snd) <$> runApp (app' . _appIn_event) () (map (fmap That) input)

-- | same as runApp' except only returns sampled output behavior
-- see comments for 'tickAppFrame'
runAppB
  :: (t ~ SpiderTimeline Global, m ~ SpiderHost Global)
  => (Event t eIn -> TestGuestT t m (Behavior t bOut))
  -> [Maybe eIn]
  -> IO [[bOut]]
runAppB app input = do
  let app' = fmap (flip AppOut never) . app
  map (map fst) <$> runApp (app' . _appIn_event) () (map (fmap That) input)