{-# LANGUAGE DeriveGeneric #-}
-- | Client-specific game state components.
module Game.LambdaHack.Client.State
  ( StateClient(..), AlterLid, BfsAndPath(..), TgtAndPath(..)
  , emptyStateClient, cycleMarkSuspect
  , updateTarget, getTarget, updateLeader, sside, sleader
  ) where

import Prelude ()

import Game.LambdaHack.Common.Prelude

import           Data.Binary
import qualified Data.EnumMap.Lazy as LEM
import qualified Data.EnumMap.Strict as EM
import qualified Data.EnumSet as ES
import qualified Data.Map.Strict as M
import           GHC.Generics (Generic)
import qualified System.Random as R

import           Game.LambdaHack.Atomic
import           Game.LambdaHack.Client.Bfs
import           Game.LambdaHack.Client.ClientOptions
import           Game.LambdaHack.Common.Actor
import           Game.LambdaHack.Common.ActorState
import           Game.LambdaHack.Common.Faction
import           Game.LambdaHack.Common.Item
import           Game.LambdaHack.Common.Kind
import           Game.LambdaHack.Common.Level
import           Game.LambdaHack.Common.Perception
import           Game.LambdaHack.Common.Point
import qualified Game.LambdaHack.Common.PointArray as PointArray
import           Game.LambdaHack.Common.State
import           Game.LambdaHack.Content.ModeKind (ModeKind)

-- | Client state, belonging to a single faction.
data StateClient = StateClient
  { seps          :: Int            -- ^ a parameter of the aiming digital line
  , stargetD      :: EM.EnumMap ActorId TgtAndPath
                                    -- ^ targets of our actors in the dungeon
  , sfleeD        :: EM.EnumMap ActorId Point
                                    -- ^ the position when fleeing requested
  , sexplored     :: ES.EnumSet LevelId
                                    -- ^ the set of fully explored levels
  , sbfsD         :: EM.EnumMap ActorId BfsAndPath
                                    -- ^ pathfinding data for our actors
  , sundo         :: [CmdAtomic]    -- ^ atomic commands performed to date
  , sdiscoBenefit :: DiscoveryBenefit
      -- ^ remembered AI benefits of items; could be recomputed at resume,
      --   but they are costly to generate and not too large
  , sfper         :: PerLid         -- ^ faction perception indexed by level
  , salter        :: AlterLid       -- ^ cached alter ability data for positions
  , srandom       :: R.StdGen       -- ^ current random generator
  , _sleader      :: Maybe ActorId  -- ^ candidate new leader of the faction;
                                    --   Faction.gleader is the old leader
  , _sside        :: FactionId      -- ^ faction controlled by the client
  , squit         :: Bool           -- ^ exit the game loop
  , scurChal      :: Challenge      -- ^ current game challenge setup
  , snxtChal      :: Challenge      -- ^ next game challenge setup
  , snxtScenario  :: Int            -- ^ next game scenario number
  , smarkSuspect  :: Int            -- ^ whether to mark suspect features
  , scondInMelee  :: LEM.EnumMap LevelId Bool
                                    -- ^ whether we are in melee, per level
  , svictories    :: EM.EnumMap (ContentId ModeKind) (M.Map Challenge Int)
                                    -- ^ won games at particular difficulty lvls
  , soptions      :: ClientOptions  -- ^ client options
  }
  deriving Show

type AlterLid = EM.EnumMap LevelId (PointArray.Array Word8)

-- | Pathfinding distances to all reachable positions of an actor
-- and a shortest paths to some of the positions.
data BfsAndPath =
    BfsInvalid
  | BfsAndPath { bfsArr  :: PointArray.Array BfsDistance
               , bfsPath :: EM.EnumMap Point AndPath
               }
  deriving Show

-- | Actor's target and a path to it, if any.
data TgtAndPath = TgtAndPath {tapTgt :: Target, tapPath :: AndPath}
  deriving (Show, Generic)

instance Binary TgtAndPath

-- | Initial empty game client state.
emptyStateClient :: FactionId -> StateClient
emptyStateClient _sside =
  StateClient
    { seps = fromEnum _sside
    , stargetD = EM.empty
    , sfleeD = EM.empty
    , sexplored = ES.empty
    , sbfsD = EM.empty
    , sundo = []
    , sdiscoBenefit = EM.empty
    , sfper = EM.empty
    , salter = EM.empty
    , srandom = R.mkStdGen 42  -- will get modified in this and future games
    , _sleader = Nothing  -- no heroes yet alive
    , _sside
    , squit = False
    , scurChal = defaultChallenge
    , snxtChal = defaultChallenge
    , snxtScenario = 0
    , smarkSuspect = 1
    , scondInMelee = LEM.empty
    , svictories = EM.empty
    , soptions = defClientOptions
    }

-- | Cycle the 'smarkSuspect' setting.
cycleMarkSuspect :: StateClient -> StateClient
cycleMarkSuspect s@StateClient{smarkSuspect} =
  s {smarkSuspect = (smarkSuspect + 1) `mod` 3}

-- | Update target parameters within client state.
updateTarget :: ActorId -> (Maybe Target -> Maybe Target) -> StateClient
             -> StateClient
updateTarget aid f cli =
  let f2 tp = case f $ fmap tapTgt tp of
        Nothing -> Nothing
        Just tgt -> Just $ TgtAndPath tgt NoPath  -- reset path
  in cli {stargetD = EM.alter f2 aid (stargetD cli)}

-- | Get target parameters from client state.
getTarget :: ActorId -> StateClient -> Maybe Target
getTarget aid cli = fmap tapTgt $ EM.lookup aid $ stargetD cli

-- | Update picked leader within state. Verify actor's faction.
updateLeader :: ActorId -> State -> StateClient -> StateClient
updateLeader leader s cli =
  let side1 = bfid $ getActorBody leader s
      side2 = sside cli
  in assert (side1 == side2 `blame` "enemy actor becomes our leader"
                            `swith` (side1, side2, leader, s))
     $ cli {_sleader = Just leader}

sside :: StateClient -> FactionId
sside = _sside

sleader :: StateClient -> Maybe ActorId
sleader = _sleader

instance Binary StateClient where
  put StateClient{..} = do
    put seps
    put stargetD
    put sfleeD
    put sexplored
    put sundo
    put sdiscoBenefit
    put (show srandom)
    put _sleader
    put _sside
    put scurChal
    put snxtChal
    put snxtScenario
    put smarkSuspect
    put scondInMelee
    put svictories
#ifdef WITH_EXPENSIVE_ASSERTIONS
    put sfper
#endif
  get = do
    seps <- get
    stargetD <- get
    sfleeD <- get
    sexplored <- get
    sundo <- get
    sdiscoBenefit <- get
    g <- get
    _sleader <- get
    _sside <- get
    scurChal <- get
    snxtChal <- get
    snxtScenario <- get
    smarkSuspect <- get
    scondInMelee <- get
    svictories <- get
    let sbfsD = EM.empty
        salter = EM.empty
        srandom = read g
        squit = False
        soptions = defClientOptions
#ifndef WITH_EXPENSIVE_ASSERTIONS
        sfper = EM.empty
#else
    sfper <- get
#endif
    return $! StateClient{..}