-------------------------------------------------------------------------------
-- Layer 1 (imperative), as per
-- https://www.parsonsmatt.org/2018/03/22/three_layer_haskell_cake.html
-- 2019 Francesco Ariis GPLv3
-------------------------------------------------------------------------------

{-# Language ScopedTypeVariables #-}

module Terminal.Game.Layer.Imperative where

import Terminal.Game.Layer.Object

import qualified Control.Concurrent   as CC
import qualified Control.Exception    as E
import qualified Control.Monad        as CM
import qualified System.IO            as SI

import Terminal.Game.Plane

-- xxx also when it goes to crash screen, it says press any key to
--     continue, yet only enter works

-- | Game definition datatype, parametrised on your gamestate. The two
-- most important elements are the function dealing with logic and the
-- drawing one. Check @alone@ (you can compile it with @cabal
-- new-run -f examples alone@) to see a simple game in action.
data Game s = Game {
        gScreenWidth   :: Width,           -- ^Gamescreen size, width.
        gScreenHeight  :: Height,          -- ^Gamescreen size, height.
        gFPS           :: FPS,             -- ^Frames per second.
        gInitState     :: s,               -- ^Initial state of the game.
        gLogicFunction :: s -> Event -> s, -- ^Logic function.
        gDrawFunction  :: s -> Plane,      -- ^Draw function.
        gQuitFunction  :: s -> Bool        -- ^\"Should I quit?\" function.
      }

-- | Entry point for the game execution, should be called in @main@.
--
-- You __must__ compile your programs with @-threaded@; if you do not do
-- this the game will crash at start-up. Just add:
--
-- @
-- ghc-options:      -threaded
-- @
--
--   in your @.cabal@ file and you will be fine!
playGame :: Game s -> IO ()
playGame g = () <$ runGIO (runGameGeneral g)

-- | Tests a game in a /pure/ environment. You can
-- supply the 'Event's yourself or use 'recordGame' to obtain them.
testGame :: Game s -> [Event] -> s
testGame g es = fst $ runTest (runGameGeneral g) (Env False es)

-- | As 'testGame', but returns 'Game' instead of a bare state.
-- Useful to fast-forward (e.g.: skip menus) before invoking 'playGame'.
setupGame :: Game s -> [Event] -> Game s
setupGame g es = let s' = testGame g es
                 in g { gInitState = s' }

-- | Similar to 'testGame', runs the game given a list of 'Events'. Unlike
-- 'testGame', the playthrough will be displayed on screen. Useful when a
-- test fails and you want to see how.
--
-- See this in action with  @cabal new-run -f examples alone-playback@.
narrateGame :: Game s -> [Event] -> IO s
narrateGame g e = runReplay (runGameGeneral g) e
    -- xxx replaygame is very difficult to test

-- -- | Play as in 'playGame' and write the session to @file@. Useful to
-- -- produce input for 'testGame' and 'replayGame'.
-- recordGame :: Game s -> FilePath -> IO s
-- recordGame g fp =
--         CC.newMVar []                   >>= \ve ->
--         runRecord (runGameGeneral g) ve >>= \s ->
--         writeMoves fp ve                >>
--         return s

-- | Play as in 'playGame' and write the session to @file@. Useful to
-- produce input for 'testGame' and 'replayGame'. Session will be
-- recorded even if an exception happens while playing.
recordGame :: Game s -> FilePath -> IO ()
recordGame g fp =
        E.bracket
          (CC.newMVar [])
          (\ve -> writeMoves fp ve)
          (\ve -> () <$ runRecord (runGameGeneral g) ve)

data Config = Config { cMEvents :: CC.MVar [Event],
                       cFPS     :: FPS              }

runGameGeneral :: forall s m. MonadGameIO m =>
                  Game s -> m s
runGameGeneral (Game gw gh fps s lf df qf) =
            -- init
            sizeAssert gw gh >>
            setupDisplay     >>
            startEvents fps  >>= \(InputHandle ve ts) ->

            -- do it!
            let c = Config ve fps in
            cleanUpErr (game c)
                            -- this under will be run regardless
                       (stopEvents ts >>
                        shutdownDisplay  )
    where
          game :: MonadGameIO m => Config -> m s
          game c = gameLoop gw gh c
                            s lf df qf
                            Nothing (0,0)


-- | Wraps an @IO@ computation so that any 'error' gets displayed along
-- with a @\<press any key to quit\>@ prompt.
-- Some terminals shut-down immediately upon program end: 'errorPress'
-- makes it easier to beta-test games on those terminals.
errorPress :: IO a -> IO a
errorPress m = E.catch m errorDisplay
    where
          errorDisplay :: E.ErrorCall -> IO a
          errorDisplay (E.ErrorCallWithLocation cs l) =
              putStrLn "ERROR REPORT\n"      >>
              putStrLn (cs ++ "\n\n")        >>
              putStrLn "Stack trace info:\n" >>
              putStrLn (l ++ "\n")           >>

              putStrLn "\n <Press any key to quit>"    >>
              SI.hSetBuffering SI.stdin SI.NoBuffering >>
              getChar                                  >>
              errorWithoutStackTrace "errorPress"


-----------
-- LOGIC --
-----------

-- from http://www.loomsoft.net/resources/alltut/alltut_lesson6.htm
gameLoop :: MonadGameIO m     =>
            Width             -> -- gamewidth
            Height            -> -- gameheight
            Config            -> -- event source
            s                 -> -- state
            (s -> Event -> s) -> -- logic function
            (s -> Plane)      -> -- draw function
            (s -> Bool)       -> -- quit? function
            Maybe Plane       -> -- last blitted screen
            (Width, Height)   -> -- Term Dimensions
            m s
gameLoop gw gh c s lf df qf opln td =

        -- quit?
        checkQuit qf s >>= \qb ->
        if qb
          then return s
        else

        -- fetch events (if any)
        pollEvents (cMEvents c) >>= \es ->
            -- xxx test poll events si rompe se lo sposto su

        -- no events? skip everything
        if null es
          then sleepABit (cFPS c)               >>
               gameLoop gw gh c s lf df qf opln td
               -- xxx reader monad qui
        else

        -- logic
        let s' = stepsLogic s lf es in

        -- clear screen if resolution change
        displaySize               >>= \td'@(tw, th) ->
        let resc = td /= td' in
        CM.when resc clearDisplay >>

        -- draw
        -- xxx solo se è tick e non kpress? [loop]
        let opln' | resc = Nothing -- res changed? restart double buffering
                  | otherwise = opln
            gpl   = blankPlane gw gh
            npln  = pastePlane (df s') gpl (1, 1) in
        blitPlane tw th opln' npln >>

        gameLoop gw gh c s' lf df qf (Just npln) td'


stepsLogic :: s -> (s -> Event -> s) -> [Event] -> s
stepsLogic s lf es = foldl lf s es


-----------------
-- ANCILLARIES --
-----------------

sizeAssert :: MonadDisplay m => Width -> Height -> m ()
sizeAssert gw gh =
        displaySize >>= \(sw, sh) ->

        let errMess =
               "This games requires a screen of " ++ show gw ++
               " columns and " ++ show gh ++ " rows.\n" ++
               "Yours only has " ++ show sw ++ " columns and " ++
               show sh ++ " rows!\n\n" ++
               "Please resize your terminal and relaunch " ++
               "the game!\n"
        in

        CM.when (gw > sw || gh > sh)
                (error errMess)