content: -- | Common client monad operations.
  module Game.LambdaHack.Client.CommonM
    ( getPerFid, aidTgtToPos, makeLine
    , currentSkillsClient, pickWeaponClient
    , updateSalter, createSalter
    ) where
  
  import Prelude ()
  
  import Game.LambdaHack.Core.Prelude
  
  import qualified Data.EnumMap.Strict as EM
  
  import           Game.LambdaHack.Client.MonadClient
  import           Game.LambdaHack.Client.Request
  import           Game.LambdaHack.Client.State
  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.ItemAspect as IA
  import           Game.LambdaHack.Common.Kind
  import           Game.LambdaHack.Common.Level
  import           Game.LambdaHack.Common.MonadStateRead
  import           Game.LambdaHack.Common.Perception
  import           Game.LambdaHack.Common.Point
  import qualified Game.LambdaHack.Common.PointArray as PointArray
  import           Game.LambdaHack.Common.State
  import qualified Game.LambdaHack.Common.Tile as Tile
  import           Game.LambdaHack.Common.Types
  import           Game.LambdaHack.Common.Vector
  import           Game.LambdaHack.Content.RuleKind
  import           Game.LambdaHack.Content.TileKind (TileKind, isUknownSpace)
  import           Game.LambdaHack.Core.Random
  import qualified Game.LambdaHack.Definition.Ability as Ability
  import           Game.LambdaHack.Definition.Defs
  
  -- | Get the current perception of a client.
  getPerFid :: MonadClientRead m => LevelId -> m Perception
  getPerFid lid = do
    fper <- getsClient sfper
    let assFail = error $ "no perception at given level"
                          `showFailure` (lid, fper)
    return $! EM.findWithDefault assFail lid fper
  
  -- | Calculate the position of an actor's target.
  -- This matches @pathGoal@, but sometimes path is not defined.
  aidTgtToPos :: Maybe ActorId -> LevelId -> Maybe Target -> State -> Maybe Point
  aidTgtToPos _ _ Nothing _ = Nothing
  aidTgtToPos maid lidV (Just tgt) s = case tgt of
    TEnemy a ->
      let body = getActorBody a s
      in if blid body == lidV then Just (bpos body) else Nothing
    TNonEnemy a ->
      let body = getActorBody a s
      in if blid body == lidV then Just (bpos body) else Nothing
    TPoint _ lid p ->
      if lid == lidV then Just p else Nothing
    TVector v -> case maid of
      Nothing -> Nothing
      Just aid ->
        let COps{corule=RuleContent{rWidthMax, rHeightMax}} = scops s
            b = getActorBody aid s
            shifted = shiftBounded rWidthMax rHeightMax (bpos b) v
        in if shifted == bpos b && v /= Vector 0 0 then Nothing else Just shifted
  
  -- | Counts the number of steps until the projectile would hit a non-projectile
  -- actor or obstacle. Starts searching with the given eps and returns
  -- the first found eps for which the number reaches the distance between
  -- actor and target position, or Nothing if none can be found.
  -- Treats unknown tiles as walkable, but prefers known.
  makeLine :: Bool -> Actor -> Point -> Int -> COps -> Level -> Maybe Int
  makeLine onlyFirst body fpos epsOld cops lvl =
    let COps{coTileSpeedup} = cops
        dist = chessDist (bpos body) fpos
        calcScore :: Int -> Int
        calcScore eps = case bresenhamsLineAlgorithm eps (bpos body) fpos of
          Just bl ->
            let blDist = take (dist - 1) bl  -- goal not checked; actor well aware
                noActor p = p == fpos || not (occupiedBigLvl p lvl)
                accessibleUnknown tpos =
                  let tt = lvl `at` tpos
                  in Tile.isWalkable coTileSpeedup tt || isUknownSpace tt
                accessU = all noActor blDist
                          && all accessibleUnknown blDist
                accessFirst | not onlyFirst = False
                            | otherwise =
                  all noActor (take 1 blDist)
                  && all accessibleUnknown (take 1 blDist)
                nUnknown = length $ filter (isUknownSpace . (lvl `at`)) blDist
            in if | accessU -> - nUnknown
                  | accessFirst -> -10000
                  | otherwise -> minBound
          Nothing -> error $ "" `showFailure` (body, fpos, epsOld)
        tryLines :: Int -> (Maybe Int, Int) -> Maybe Int
        tryLines curEps (acc, _) | curEps == epsOld + dist = acc
        tryLines curEps (acc, bestScore) =
          let curScore = calcScore curEps
              newAcc = if curScore > bestScore
                       then (Just curEps, curScore)
                       else (acc, bestScore)
          in tryLines (curEps + 1) newAcc
    in if | dist <= 0 -> Nothing  -- ProjectAimOnself
          | calcScore epsOld > minBound -> Just epsOld  -- keep old
          | otherwise -> tryLines (epsOld + 1) (Nothing, minBound)  -- find best
  
  -- @MonadStateRead@ would be enough, but the logic is sound only on client.
  currentSkillsClient :: MonadClientRead m => ActorId -> m Ability.Skills
  currentSkillsClient aid = do
    body <- getsState $ getActorBody aid
    side <- getsClient sside
    -- Newest Leader in sleader, not yet in sfactionD.
    mleader <- if bfid body == side
               then getsClient sleader
               else do
                 fact <- getsState $ (EM.! bfid body) . sfactionD
                 return $! gleader fact
    getsState $ actorCurrentSkills mleader aid  -- keep it lazy
  
  -- Client has to choose the weapon based on its partial knowledge,
  -- because if server chose it, it would leak item discovery information.
  --
  -- Note that currently the aspects of the target actor are not considered,
  -- because all weapons share the sum of all source actor aspects and only differ
  -- in damage (equally important for all targets) and effects (really hard
  -- to tell which is better for which target or even which is better
  -- for the same target, so it's random). If only individual weapon's +toHit
  -- was applied to the target, situation would be much more complex,
  -- which is precisely why we keep it as is and let the player make choices
  -- by equipping and unequipping weapons instead. Content should ensure
  -- that the rule of thumb (which AI uses) that more weapons is better
  -- should give good results almost always, at least at the start of the game,
  -- to limit micromanagement and to spare newbies.
  --
  -- Note that situation is completely different with choosing projectiles
  -- against a particular foe, even before (potential) splash damage
  -- that hits multiple tagets comes into the equation. AI has to be very
  -- primitive and random here as well.
  pickWeaponClient :: MonadClient m
                   => ActorId -> ActorId -> m (Maybe RequestTimed)
  pickWeaponClient source target = do
    eqpAssocs <- getsState $ kitAssocs source [CEqp]
    bodyAssocs <- getsState $ kitAssocs source [COrgan]
    actorSk <- currentSkillsClient source
    tb <- getsState $ getActorBody target
    let kitAssRaw = eqpAssocs ++ bodyAssocs
        kitAss = filter (IA.checkFlag Ability.Meleeable
                         . aspectRecordFull . fst . snd) kitAssRaw
        benign itemFull = let arItem = aspectRecordFull itemFull
                          in IA.checkFlag Ability.Benign arItem
    discoBenefit <- getsClient sdiscoBenefit
    strongest <- pickWeaponM False (Just discoBenefit) kitAss actorSk source
    case strongest of
      [] -> return Nothing
      (_, _, _, _, _, (itemFull, _)) : _ | benign itemFull && bproj tb ->
        return Nothing  -- if strongest is benign, don't waste fun on a projectile
      iis@(ii1@(value1, hasEffect1, timeout1, _, _, (itemFull1, _)) : _) -> do
        let minIis = takeWhile (\(value, hasEffect, timeout, _, _, _) ->
                                   value == value1
                                   && hasEffect == hasEffect1
                                   && timeout == timeout1)
                               iis
        -- Randomize only the no-timeout items. Others need to activate
        -- in the order shown in HUD and also not risk of only one always used.
        (_, _, _, _, iid, _) <- if timeout1 > 0 || itemSuspect itemFull1
                                then return ii1
                                else rndToAction $ oneOf minIis
        -- Prefer COrgan, to hint to the player to trash the equivalent CEqp item.
        let cstore = if isJust (lookup iid bodyAssocs) then COrgan else CEqp
        return $ Just $ ReqMelee target iid cstore
  
  updateSalter :: MonadClient m
               => LevelId -> [(Point, ContentId TileKind)] -> m ()
  updateSalter lid pts = do
    COps{coTileSpeedup} <- getsState scops
    let pas = map (second $ toEnum . Tile.alterMinWalk coTileSpeedup) pts
        f = (PointArray.// pas)
    modifyClient $ \cli -> cli {salter = EM.adjust f lid $ salter cli}
  
  createSalter :: State -> AlterLid
  createSalter s =
    let COps{coTileSpeedup} = scops s
        f Level{ltile} =
          PointArray.mapA (toEnum . Tile.alterMinWalk coTileSpeedup) ltile
    in EM.map f $ sdungeon s
  