-- | Ways for the client to use player input via UI to produce server
-- requests, based on the client's view (visualized for the player)
-- of the game state.
--
-- This module is leaking quite a bit of implementation details
-- for the sake of "Game.LambdaHack.Client.LoopM". After multiplayer
-- is enabled again and the new requirements sorted out, this should be
-- redesigned and some code moved down the module hierarhy tree,
-- exposing a smaller API here.
module Game.LambdaHack.Client.UI
  ( -- * Querying the human player
    queryUI, queryUIunderAI
    -- * UI monad operations
  , MonadClientUI(..), putSession, anyKeyPressed, resetPressedKeys
    -- * UI session type
  , SessionUI(..), ReqDelay(..), emptySessionUI
    -- * Updating UI state wrt game state changes
  , watchRespUpdAtomicUI, watchRespSfxAtomicUI
    -- * Startup and initialization
  , CCUI(..)
  , UIOptions, applyUIOptions, uOverrideCmdline, mkUIOptions
    -- * Assorted operations and types
  , ChanFrontend, chanFrontend, tryRestore, clientPrintUI
  , pushReportFrame, msgAdd, MsgClassShow(..)
#ifdef EXPOSE_INTERNAL
    -- * Internal operations
  , stepQueryUIwithLeader, stepQueryUI
#endif
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import qualified Data.EnumMap.Strict as EM
import qualified Data.EnumSet as ES
import qualified Data.Map.Strict as M
import qualified Data.Text as T

import           Game.LambdaHack.Client.MonadClient
import           Game.LambdaHack.Client.Request
import           Game.LambdaHack.Client.State
import           Game.LambdaHack.Client.UI.Content.Input
import           Game.LambdaHack.Client.UI.ContentClientUI
import           Game.LambdaHack.Client.UI.Frame
import           Game.LambdaHack.Client.UI.FrameM
import           Game.LambdaHack.Client.UI.Frontend
import           Game.LambdaHack.Client.UI.HandleHelperM
import           Game.LambdaHack.Client.UI.HandleHumanM
import qualified Game.LambdaHack.Client.UI.Key as K
import           Game.LambdaHack.Client.UI.MonadClientUI
import           Game.LambdaHack.Client.UI.Msg
import           Game.LambdaHack.Client.UI.MsgM
import           Game.LambdaHack.Client.UI.Overlay
import           Game.LambdaHack.Client.UI.SessionUI
import           Game.LambdaHack.Client.UI.Slideshow
import           Game.LambdaHack.Client.UI.SlideshowM
import           Game.LambdaHack.Client.UI.UIOptions
import           Game.LambdaHack.Client.UI.UIOptionsParse
import           Game.LambdaHack.Client.UI.Watch
import           Game.LambdaHack.Common.Actor
import           Game.LambdaHack.Common.ActorState
import           Game.LambdaHack.Common.ClientOptions
import           Game.LambdaHack.Common.Faction
import           Game.LambdaHack.Common.MonadStateRead
import           Game.LambdaHack.Common.State
import           Game.LambdaHack.Content.FactionKind

-- | Handle the move of a human player.
queryUI :: (MonadClient m, MonadClientUI m) => m (Maybe RequestUI)
queryUI = do
  sreqQueried <- getsSession sreqQueried
  let !_A = assert (not sreqQueried) ()  -- querying not nested
  modifySession $ \sess -> sess {sreqQueried = True}
  let loop = do
        mres <- stepQueryUIwithLeader
        saimMode <- getsSession saimMode
        case mres of
          Nothing | isJust saimMode -> loop  -- loop until aiming finished
          _ -> return mres
  mres <- loop
  modifySession $ \sess -> sess {sreqQueried = False}
  return mres

queryUIunderAI :: (MonadClient m, MonadClientUI m) => m RequestUI
queryUIunderAI = do
 -- Record history so the player can browse it later on.
 recordHistory
 -- As long as UI faction is under AI control, check, once per move,
 -- for immediate control regain or benchmark game stop.
 sregainControl <- getsSession sregainControl
 if sregainControl then do
   modifySession $ \sess -> sess { sregainControl = False
                                 , sreqDelay = ReqDelayNot
                                 , sreqPending = Nothing }  -- just in case
   -- The keys mashed to gain control are not considered a command.
   resetPressedKeys
   -- Menu is entered in @displayRespUpdAtomicUI@ at @UpdAutoFaction@
   -- and @stopAfter@ is canceled in @cmdAtomicSemCli@
   -- when handling the results of the request below.
   return (ReqUIAutomate, Nothing)
 else do
  stopAfterFrames <- getsClient $ sstopAfterFrames . soptions
  bench <- getsClient $ sbenchmark . soptions
  let exitCmd = if bench then ReqUIGameDropAndExit else ReqUIGameSaveAndExit
  case stopAfterFrames of
    Nothing -> do
      stopAfterSeconds <- getsClient $ sstopAfterSeconds . soptions
      case stopAfterSeconds of
        Nothing -> return (ReqUINop, Nothing)
        Just stopS -> do
          sstartPOSIX <- getsSession sstart
          exit <- elapsedSessionTimeGT sstartPOSIX stopS
          if exit then do
            tellAllClipPS
            return (exitCmd, Nothing)  -- ask server to exit
          else return (ReqUINop, Nothing)
    Just stopF -> do
      allNframes <- getsSession sallNframes
      gnframes <- getsSession snframes
      if allNframes + gnframes >= stopF then do
        tellAllClipPS
        return (exitCmd, Nothing)  -- ask server to exit
      else return (ReqUINop, Nothing)

stepQueryUIwithLeader :: (MonadClient m, MonadClientUI m)
                       => m (Maybe RequestUI)
stepQueryUIwithLeader = do
  side <- getsClient sside
  mleader <- getsState $ gleader . (EM.! side) . sfactionD
  mreq <- stepQueryUI
  case mreq of
    Nothing -> return Nothing
    Just req -> do
      mleader2 <- getsClient sleader
      -- Don't send the leader switch to the server with these commands,
      -- to avoid leader death at resume if his HP <= 0. That would violate
      -- the principle that save and reload doesn't change game state.
      let saveCmd cmd = case cmd of
            ReqUIGameDropAndExit -> True
            ReqUIGameSaveAndExit -> True
            ReqUIGameSave -> True
            _ -> False
      return $ Just (req, if mleader /= mleader2 && not (saveCmd req)
                          then mleader2
                          else Nothing)

-- | Let the human player issue commands until any command takes time.
stepQueryUI :: (MonadClient m, MonadClientUI m) => m (Maybe ReqUI)
stepQueryUI = do
  FontSetup{propFont} <- getFontSetup
  keyPressed <- anyKeyPressed
  macroFrame <- getsSession smacroFrame
  -- This message, in particular, disturbs.
  when (keyPressed && not (null (unKeyMacro (keyPending macroFrame)))) $
    msgAdd MsgActionWarning "*interrupted*"
  report <- getsSession $ newReport . shistory
  modifySession $ \sess -> sess {sreportNull = nullVisibleReport report}
  slides <- reportToSlideshowKeepHalt False []
  ovs <- case unsnocSlideshow slides of
    Nothing -> return EM.empty
    Just (allButLast, (ov, _)) ->
      if allButLast == emptySlideshow
      then do
        -- Display the only generated slide while waiting for next key.
        -- Strip the "--end-" prompt from it, by ignoring @MonoFont@.
        let ovProp = ov EM.! propFont
        return $!
          EM.singleton propFont $ if EM.size ov > 1 then ovProp else init ovProp
      else do
        -- Show, one by one, all slides, awaiting confirmation for each.
        void $ getConfirms ColorFull [K.spaceKM, K.escKM] slides
        -- Indicate that report wiped out.
        modifySession $ \sess -> sess {sreportNull = True}
        -- Display base frame at the end.
        return EM.empty
  mleader <- getsClient sleader
  case mleader of
    Nothing -> return ()
    Just leader -> do
      body <- getsState $ getActorBody leader
      lastLost <- getsSession slastLost
      if bhp body <= 0 then do
        side <- getsClient sside
        fact <- getsState $ (EM.! side) . sfactionD
        let gameOver = maybe False ((/= Camping) . stOutcome) (gquit fact)
        when (not gameOver && leader `ES.notMember` lastLost) $ do
          -- Hacky reuse of @slastLost@ for near-death spam prevention.
          modifySession $ \sess ->
            sess {slastLost = ES.insert leader lastLost}
          displayMore ColorBW "If you move, the exertion will kill you. Consider asking for first aid instead."
      else
        modifySession $ \sess -> sess {slastLost = ES.empty}
  km <- promptGetKey ColorFull ovs False []
  abortOrCmd <- do
    -- Look up the key.
    CCUI{coinput=InputContent{bcmdMap}} <- getsSession sccui
    case km `M.lookup` bcmdMap of
      Just (_, _, cmd) -> do
        modifySession $ \sess -> sess {swaitTimes = if swaitTimes sess > 0
                                                    then - swaitTimes sess
                                                    else 0}
        cmdSemInCxtOfKM km cmd
      _ -> let msgKey = "unknown command '" <> K.showKM km <> "'"
           in weaveJust <$> failWith (T.pack msgKey)
  -- GC macro stack if there are no actions left to handle,
  -- removing all unnecessary macro frames at once,
  -- but leaving the last one for user's in-game macros.
  modifySession $ \sess ->
    let (smacroFrameNew, smacroStackMew) =
          dropEmptyMacroFrames (smacroFrame sess) (smacroStack sess)
    in sess { smacroFrame = smacroFrameNew
            , smacroStack = smacroStackMew }
  -- The command was failed or successful and if the latter,
  -- possibly took some time.
  case abortOrCmd of
    Right cmdS ->
      -- Exit the loop and let other actors act. No next key needed
      -- and no report could have been generated.
      return $ Just cmdS
    Left Nothing -> return Nothing
    Left (Just err) -> do
      msgAdd MsgActionAlert $ showFailError err
      return Nothing
