content: module SessionUIMock
    ( unwindMacros
    ) where
  
  import Prelude ()
  
  import Game.LambdaHack.Core.Prelude
  
  import           Control.Monad.Trans.Class
  import           Control.Monad.Trans.State.Lazy
  import           Control.Monad.Trans.Writer.Lazy
  import           Data.Bifunctor (bimap)
  import qualified Data.Map.Strict as M
  
  import qualified Game.LambdaHack.Client.UI.Content.Input as IC
  import           Game.LambdaHack.Client.UI.ContentClientUI
  import           Game.LambdaHack.Client.UI.FrameM
  import           Game.LambdaHack.Client.UI.HandleHumanLocalM
  import           Game.LambdaHack.Client.UI.HandleHumanM
  import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd
  import qualified Game.LambdaHack.Client.UI.Key as K
  import           Game.LambdaHack.Client.UI.SessionUI
    (KeyMacro (..), KeyMacroFrame (..), emptyMacroFrame)
  
  data SessionUIMock = SessionUIMock
    { smacroFrame :: KeyMacroFrame
    , smacroStack :: [KeyMacroFrame]
    , sccui       :: CCUI
    , unwindTicks :: Int
    }
  
  type KeyMacroBufferMock = Either String String
  type KeyPendingMock = String
  type KeyLastMock = String
  
  type BufferTrace = [(KeyMacroBufferMock, KeyPendingMock, KeyLastMock)]
  type ActionLog = String
  
  data Op = Looped | HeadEmpty
  
  humanCommandMock :: WriterT [(BufferTrace, ActionLog)] (State SessionUIMock) ()
  humanCommandMock = do
    abuffs <- lift $ do
      sess <- get
      return $ renderTrace (smacroFrame sess : smacroStack sess)  -- log session
    abortOrCmd <- lift iterationMock -- do stuff
    -- 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.
    lift $ modify $ \sess ->
            let (smacroFrameNew, smacroStackMew) =
                  dropEmptyMacroFrames (smacroFrame sess) (smacroStack sess)
            in sess { smacroFrame = smacroFrameNew
                    , smacroStack = smacroStackMew }
    case abortOrCmd of
      Left Looped -> void $ tell [(abuffs, "Macro looped")]
      Left HeadEmpty -> void $ tell [(abuffs, "")]  -- exit loop
      Right Nothing -> tell [(abuffs, "")] >> humanCommandMock
      Right (Just out) -> tell [(abuffs, show out)] >> humanCommandMock
  
  iterationMock :: State SessionUIMock (Either Op (Maybe K.KM))
  iterationMock = do
    SessionUIMock _ _ CCUI{coinput=IC.InputContent{bcmdMap}} ticks <- get
    if ticks <= 1000
    then do
      modify $ \sess -> sess {unwindTicks = ticks + 1}
      mkm <- promptGetKeyMock
      case mkm of
        Nothing -> return $ Left HeadEmpty  -- macro finished
        Just km -> case km `M.lookup` bcmdMap of
          Just (_, _, cmd) -> Right <$> cmdSemInCxtOfKMMock km cmd
          _ -> return $ Right $ Just km  -- unknown command; fine for tests
    else return $ Left Looped
  
  cmdSemInCxtOfKMMock :: K.KM -> HumanCmd.HumanCmd
                      -> State SessionUIMock (Maybe K.KM)
  cmdSemInCxtOfKMMock km cmd = do
    modify $ \sess ->
      sess {smacroFrame = updateKeyLast km cmd $ smacroFrame sess}
    cmdSemanticsMock km cmd
  
  cmdSemanticsMock :: K.KM -> HumanCmd.HumanCmd
                   -> State SessionUIMock (Maybe K.KM)
  cmdSemanticsMock km = \case
    HumanCmd.Macro s -> do
      modify $ \sess ->
        let kms = K.mkKM <$> s
            (smacroFrameNew, smacroStackMew) =
               macroHumanTransition kms (smacroFrame sess) (smacroStack sess)
        in sess { smacroFrame = smacroFrameNew
                , smacroStack = smacroStackMew }
      return Nothing
    HumanCmd.Repeat n -> do
      modify $ \sess ->
        let (smacroFrameNew, smacroStackMew) =
               repeatHumanTransition n (smacroFrame sess) (smacroStack sess)
        in sess { smacroFrame = smacroFrameNew
                , smacroStack = smacroStackMew }
      return Nothing
    HumanCmd.RepeatLast n -> do
      modify $ \sess ->
        sess {smacroFrame = repeatLastHumanTransition n (smacroFrame sess) }
      return Nothing
    HumanCmd.Record -> do
      modify $ \sess ->
        sess {smacroFrame = fst $ recordHumanTransition (smacroFrame sess) }
      return Nothing
    _ -> return $ Just km
  
  promptGetKeyMock :: State SessionUIMock (Maybe K.KM)
  promptGetKeyMock = do
    SessionUIMock macroFrame _ CCUI{coinput=IC.InputContent{bcmdMap}} _ <- get
    case keyPending macroFrame of
      KeyMacro (km : kms) -> do
          modify $ \sess ->
            sess {smacroFrame = (smacroFrame sess) {keyPending = KeyMacro kms}}
          modify $ \sess ->
            sess {smacroFrame = addToMacro bcmdMap km $ smacroFrame sess}
          return (Just km)
      KeyMacro [] -> return Nothing
  
  unwindMacrosFull :: IC.InputContent -> KeyMacro -> [(BufferTrace, ActionLog)]
  unwindMacrosFull coinput keyPending =
    let initSession = SessionUIMock
          { smacroFrame = emptyMacroFrame {keyPending}
          , smacroStack = []
          , sccui = emptyCCUI {coinput}
          , unwindTicks = 0 }
    in evalState (execWriterT humanCommandMock) initSession
  
  accumulateActions :: [(BufferTrace, ActionLog)] -> [(BufferTrace, ActionLog)]
  accumulateActions ba =
    let (buffers, actions) = unzip ba
        actionlog = concat <$> inits actions
    in if snd (last ba) == "Macro looped"
       then ba
       else zip buffers actionlog
  
  unwindMacros :: IC.InputContent -> KeyMacro -> [(BufferTrace, ActionLog)]
  unwindMacros coinput keyPending =
    accumulateActions $ unwindMacrosFull coinput keyPending
  
  renderTrace :: [KeyMacroFrame] -> BufferTrace
  renderTrace macroFrames =
    let buffers = bimap (concatMap K.showKM)
                        (concatMap K.showKM . unKeyMacro)
                  . keyMacroBuffer <$> macroFrames
        pendingKeys = concatMap K.showKM . unKeyMacro . keyPending <$> macroFrames
        lastKeys = maybe "" K.showKM . keyLast <$> macroFrames
    in zip3 buffers pendingKeys lastKeys
  