-- | Semantics of most 'CmdClientAI' client commands.
module Game.LambdaHack.Client.ClientSem where

import Control.Monad
import Control.Monad.Writer.Strict (runWriterT)
import qualified Data.EnumMap.Strict as EM
import qualified Data.Map.Strict as M
import Data.Maybe
import qualified Data.Text as T

import Game.LambdaHack.Client.Action
import Game.LambdaHack.Client.Binding
import Game.LambdaHack.Client.Draw
import Game.LambdaHack.Client.HumanCmd
import Game.LambdaHack.Client.HumanLocal
import Game.LambdaHack.Client.HumanSem
import Game.LambdaHack.Client.RunAction
import Game.LambdaHack.Client.State
import Game.LambdaHack.Client.Strategy
import Game.LambdaHack.Client.StrategyAction
import qualified Game.LambdaHack.Common.Ability as Ability
import Game.LambdaHack.Common.Action
import Game.LambdaHack.Common.Actor
import Game.LambdaHack.Common.ActorState
import Game.LambdaHack.Common.Faction
import Game.LambdaHack.Common.Item
import qualified Game.LambdaHack.Common.Key as K
import qualified Game.LambdaHack.Common.Kind as Kind
import Game.LambdaHack.Common.Level
import Game.LambdaHack.Common.Msg
import Game.LambdaHack.Common.Perception
import Game.LambdaHack.Common.Point
import Game.LambdaHack.Common.Random
import Game.LambdaHack.Common.ServerCmd
import Game.LambdaHack.Common.State
import Game.LambdaHack.Common.Vector
import Game.LambdaHack.Content.FactionKind
import Game.LambdaHack.Content.RuleKind
import Control.Exception.Assert.Sugar
import Game.LambdaHack.Utils.Frequency

queryAI :: MonadClient m => ActorId -> m CmdSerTakeTime
queryAI oldAid = do
  Kind.COps{cofaction=Kind.Ops{okind}, corule} <- getsState scops
  side <- getsClient sside
  fact <- getsState $ \s -> sfactionD s EM.! side
  let abilityLeader = fAbilityLeader $ okind $ gkind fact
      abilityOther = fAbilityOther $ okind $ gkind fact
  mleader <- getsClient _sleader
  if -- Keep the leader: only a leader is allowed to pick another leader.
     mleader /= Just oldAid
     -- Keep the leader: abilities are the same (we assume leader can do
     -- at least as much as others).
     || abilityLeader == abilityOther
     -- Keep the leader: other members can't melee.
     || Ability.Melee `notElem` abilityOther
    then queryAIPick oldAid
    else do
      fper <- getsClient sfper
      oldBody <- getsState $ getActorBody oldAid
      oldAis <- getsState $ getActorItem oldAid
      btarget <- getsClient $ getTarget oldAid
      let arena = blid oldBody
      -- Visibility ignored --- every foe is visible by somebody.
      foes <- getsState $ actorNotProjList (isAtWar fact) arena
      ours <- getsState $ actorNotProjAssocs (== side) arena
      time <- getsState $ getLocalTime arena
      Level{lxsize, lysize} <- getsState $ \s -> sdungeon s EM.! arena
      actorD <- getsState sactorD
      let oldPos = bpos oldBody
          per = fper EM.! arena
          posOnLevel b | blid b /= arena = Nothing
                       | otherwise = Just $ bpos b
          mfoePos foe = maybe Nothing posOnLevel
                        $ EM.lookup foe actorD
          canSee foe = maybe False (actorSeesPos per oldAid) $ mfoePos foe
          isAmmo i = jsymbol i `elem` ritemProject (Kind.stdRuleset corule)
          hasAmmo = any (isAmmo . snd) oldAis
          isAdjacent = foesAdjacent lxsize lysize oldPos foes
      if -- Keep the leader: he is alone on the level.
         length ours == 1
         -- Keep the leader: he has an enemy target.
         || case btarget of
              Just (TEnemy foe _) ->
                -- and he can shoot it.
                canSee foe && hasAmmo && not isAdjacent
              _ -> False
         -- Keep the leader: he probably just used stairs.
         || bpos oldBody == boldpos oldBody
            && not (waitedLastTurn oldBody time)
        then queryAIPick oldAid
        else do
          let countMinFoeDist (aid, b) =
                let distB = chessDist lxsize (bpos b)
                    foeDist = map (distB . bpos) foes
                    minFoeDist | null foeDist = maxBound
                               | otherwise = minimum foeDist
                in ((aid, b), minFoeDist)
              oursMinFoeDist = map countMinFoeDist ours
              inMelee (_, minFoeDist) = minFoeDist == 1
              oursMeleePos = map (bpos . snd . fst)
                             $ filter inMelee oursMinFoeDist
          let f ((aid, b), minFoeDist) =
                let distB = chessDist lxsize (bpos b)
                    meleeDist = map distB oursMeleePos
                    minMeleeDist | null meleeDist = maxBound
                                 | otherwise = minimum meleeDist
                    proximityMelee = max 0 $ 10 - minMeleeDist
                    proximityFoe = max 0 $ 20 - minFoeDist
                    distToLeader = distB oldPos
                    proximityLeader = max 0 $ 10 - distToLeader
                in if minFoeDist == 1
                      || bhp b <= 0
                      || aid == oldAid && waitedLastTurn b time
                   then -- Ignore: in melee range or incapacitated or stuck.
                        Nothing
                   else -- Help in melee, shoot or chase foes,
                        -- fan out away from each other, if too close.
                        Just ( 1
                               + proximityMelee * 9
                               + proximityFoe * 6
                               + proximityLeader * 3
                             , aid )
              candidates = mapMaybe f oursMinFoeDist
              freq | null candidates = toFreq "old leader" [(1, oldAid)]
                   | otherwise = toFreq "candidates for AI leader" candidates
          aid <- rndToAction $ frequency freq
          s <- getState
          modifyClient $ updateLeader aid s
          queryAIPick aid

queryAIPick :: MonadClient m => ActorId -> m CmdSerTakeTime
queryAIPick aid = do
  Kind.COps{cofaction=Kind.Ops{okind}} <- getsState scops
  side <- getsClient sside
  body <- getsState $ getActorBody aid
  assert (bfid body == side `blame` "AI tries to move enemy actor"
                            `twith` (aid, bfid body, side)) skip
  mleader <- getsClient _sleader
  fact <- getsState $ (EM.! bfid body) . sfactionD
  let factionAbilities
        | Just aid == mleader = fAbilityLeader $ okind $ gkind fact
        | otherwise = fAbilityOther $ okind $ gkind fact
  unless (bproj body) $ do
    stratTarget <- targetStrategy aid factionAbilities
    -- Choose a target from those proposed by AI for the actor.
    btarget <- rndToAction $ frequency $ bestVariant stratTarget
    let _debug = T.unpack
          $ "\nHandleAI abilities:" <+> showT factionAbilities
          <> ", symbol:"            <+> showT (bsymbol body)
          <> ", aid:"               <+> showT aid
          <> ", pos:"               <+> showT (bpos body)
          <> "\nHandleAI starget:"  <+> showT stratTarget
          <> "\nHandleAI target:"   <+> showT btarget
--    trace _debug skip
    modifyClient $ updateTarget aid (const btarget)
  stratAction <- actionStrategy aid factionAbilities
  -- Run the AI: chose an action from those given by the AI strategy.
  action <- rndToAction $ frequency $ bestVariant stratAction
  let _debug = T.unpack
          $ "HandleAI saction:"   <+> showT stratAction
          <> "\nHandleAI action:" <+> showT action
--  trace _debug skip
  return action

-- | Handle the move of a UI player.
queryUI :: (MonadClientAbort m, MonadClientUI m) => ActorId -> m CmdSer
queryUI aid = do
  -- When running, stop if aborted by a disturbance. Otherwise let
  -- the human player issue commands, until any of them takes time.
  leader <- getLeaderUI
  assert (leader == aid `blame` "player moves not his leader"
                        `twith` (leader, aid)) skip
  let inputHumanCmd msg = do
        stopRunning
        humanCommand msg
  tryWith inputHumanCmd $ do
    srunning <- getsClient srunning
    maybe abort (continueRun leader) srunning

-- | Continue running in the given direction.
continueRun :: MonadClientAbort m => ActorId -> (Vector, Int) -> m CmdSer
continueRun leader dd = do
  (dir, distNew) <- continueRunDir leader dd
  modifyClient $ \cli -> cli {srunning = Just (dir, distNew)}
  -- The potential invisible actor is hit. War is started without asking.
  return $ TakeTimeSer $ MoveSer leader dir

-- | Determine and process the next human player command. The argument is
-- the last abort message due to running, if any.
humanCommand :: forall m. (MonadClientAbort m, MonadClientUI m)
             => Msg
             -> m CmdSer
humanCommand msgRunAbort = do
  let loop :: Overlay -> m CmdSer
      loop overlay = do
        km <- getKeyOverlayCommand overlay
        -- Messages shown, so update history and reset current report.
        recordHistory
        -- On abort, just reset state and call loop again below.
        -- Each abort that gets this far generates a slide to be shown.
        (mcmdS, slides) <- runWriterT $ tryWithSlide (return Nothing) $ do
          -- Look up the key.
          Binding{kcmd} <- askBinding
          case M.lookup km kcmd of
            Just (_, _, cmd) -> do
              -- Query and clear the last command key.
              lastKey <- getsClient slastKey
              -- TODO: perhaps replace slastKey
              -- with test 'kmNext == km'
              -- or an extra arg to 'loop'.
              -- Depends on whether slastKey
              -- is needed in other parts of code.
              modifyClient (\st -> st {slastKey = Just km})
              cmdHumanSem $ if Just km == lastKey
                            then Clear
                            else cmd
            Nothing -> let msgKey = "unknown command <" <> K.showKM km <> ">"
                       in abortWith msgKey
        -- The command was aborted or successful and if the latter,
        -- possibly took some time.
        case mcmdS of
          Just cmdS -> do
            assert (null (runSlideshow slides)
                    `blame` "some slides generated for server command"
                    `twith` slides) skip
            -- Exit the loop and let other actors act. No next key needed
            -- and no slides could have been generated.
            modifyClient (\st -> st {slastKey = Nothing})
            return cmdS
          Nothing -> do
            -- If no time taken, rinse and repeat.
            -- Analyse the obtained slides.
            mLast <- case reverse (runSlideshow slides) of
              [] -> return Nothing
              [sLast] -> return $ Just sLast
              sls@(sLast : _) -> do
                -- Show, one by one, all slides, awaiting confirmation
                -- for all but the last one.
                -- Note: the code that generates the slides is responsible
                -- for inserting the @more@ prompt.
                go <- getInitConfirms ColorFull [km] $ toSlideshow $ reverse sls
                return $! if go then Just sLast else Nothing
            case mLast of
              Nothing -> do
                -- Display current state if no slideshow or interrupted.
                modifyClient (\st -> st {slastKey = Nothing})
                sli <- promptToSlideshow ""
                loop $! head $! runSlideshow sli
              Just sLast ->
                -- (Re-)display the last slide while waiting for the next key,
                loop sLast
  sli <- promptToSlideshow msgRunAbort
  let overlayInitial = head $ runSlideshow sli
  loop overlayInitial