{-# LANGUAGE RankNTypes #-}

module Glazier.Pipes.Ui where

import Control.Concurrent.STM
import Control.Concurrent.STM.TMVar.Extras as STE
import Control.Concurrent
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Morph
import Control.Monad.Trans.Maybe
import qualified Pipes as P
import qualified Pipes.Prelude as PP

-- | This is similar to part of the Elm startApp.
-- This is responsible for running the Glazier Gadget update tick until it quits.
-- This is also responsible for rendering the frame.
runUi :: (MonadIO io) =>
     Int
  -> (s -> IO ()) -- render
  -> P.Producer s io s
  -> io s
runUi refreshDelay render appSignal = do
    -- framerate thread
    -- TMVar to indicate that the render thread can render, start non empty so we can render straight away.
    triggerRender <- liftIO $ newTMVarIO ()
    frameRateThread <-
        liftIO $
        forkIO . void . forever $
        -- The will ensure a refreshDelay in between times when value in canRender is taken.
        -- wait until canRender is empty (ie taken by the render thread)
         do
            atomically $ STE.waitTillEmptyTMVar triggerRender ()
            -- if empty, then wait delay before filling TMVar with next canRender value
            threadDelay refreshDelay
            atomically $ putTMVar triggerRender ()

    -- render thread
    enableRenderThread <- liftIO $ newTMVarIO ()
    finishedRenderThread <- liftIO newEmptyTMVarIO
    latestState <- liftIO newEmptyTMVarIO
    void . liftIO $
        forkFinally
            (void . runMaybeT . forever $
              -- check if we can start render
              do
                 liftIO . atomically . void $ takeTMVar triggerRender
                 -- to allow rendering of last frame before quitting
                 -- if there is no state to render, check if rendering is disabled
                 s <-
                     MaybeT . liftIO . atomically $
                     (Just <$> takeTMVar latestState) `orElse` do
                         r <- tryReadTMVar enableRenderThread
                         case r of
                             Nothing -> pure Nothing -- breaks (runMaybeT . forever) loop
                             Just _ -> retry
                 lift $ render s)
            (const . atomically $ putTMVar finishedRenderThread ())

    -- This is different between the Strict and Lazy version
    s' <- P.runEffect $
        appSignal P.>-> PP.mapM
            (liftIO . atomically . void . STE.forceSwapTMVar latestState) P.>-> PP.drain
    -- cleanup
    -- allow rendering of the frame one last time
    liftIO . atomically $ takeTMVar enableRenderThread
    -- wait for render thread to finish before exiting
    liftIO . atomically $ takeTMVar finishedRenderThread
    -- kill frameRateThread only after render thread has finished
    -- since renderThread waits on triggers from frameRateThread
    liftIO $ killThread frameRateThread
    -- return final state
    liftIO $ pure s'