content: {-# LANGUAGE TupleSections #-}
  -- | The common, for server and clients, main game state type
  -- and its operations.
  module Game.LambdaHack.Common.State
    ( -- * Basic game state, local or global
      State
      -- * State components
    , sdungeon, stotalDepth, sactorD, sitemD, sitemIxMap, sfactionD, stime, scops
    , sgold, shigh, sgameModeId, sdiscoKind, sdiscoAspect, sactorMaxSkills
      -- * State construction
    , defStateGlobal, emptyState, localFromGlobal
      -- * State update
    , updateDungeon, updateDepth, updateActorD, updateItemD, updateItemIxMap
    , updateFactionD, updateTime, updateCOpsAndCachedData, updateGold
    , updateDiscoKind, updateDiscoAspect, updateActorMaxSkills
      -- * State operations
    , getItemBody, aspectRecordFromItem, aspectRecordFromIid
    , maxSkillsFromActor, maxSkillsInDungeon
  #ifdef EXPOSE_INTERNAL
      -- * Internal operations
    , unknownLevel
  #endif
      -- * Operations both internal and used in unit tests
    , unknownTileMap
    ) where
  
  import Prelude ()
  
  import Game.LambdaHack.Core.Prelude
  
  import           Data.Binary
  import qualified Data.EnumMap.Strict as EM
  
  import           Game.LambdaHack.Common.Actor
  import           Game.LambdaHack.Common.Area
  import           Game.LambdaHack.Common.Faction
  import qualified Game.LambdaHack.Common.HighScore as HighScore
  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.Point
  import qualified Game.LambdaHack.Common.PointArray as PointArray
  import           Game.LambdaHack.Common.Time
  import           Game.LambdaHack.Common.Types
  import           Game.LambdaHack.Content.CaveKind (CaveKind)
  import           Game.LambdaHack.Content.ModeKind
  import           Game.LambdaHack.Content.RuleKind
  import           Game.LambdaHack.Content.TileKind (TileKind)
  import qualified Game.LambdaHack.Content.TileKind as TK
  import qualified Game.LambdaHack.Core.Dice as Dice
  import qualified Game.LambdaHack.Definition.Ability as Ability
  import           Game.LambdaHack.Definition.Defs
  
  -- | The main game state, the basic one, pertaining to a single game,
  -- not to a single playing session or an intersection of both.
  -- This state persists between playing sessions, until the particular game ends.
  -- Anything that persists between games is stored in server state,
  -- client state or client UI session state.
  --
  -- Another differentiating property of this state is that it's kept
  -- separately on the server and each of the clients (players, human or AI)
  -- and separately updated, according to what each player can observe.
  -- It's never updated directly, but always through atomic commands
  -- ("CmdAtomic") that are filtered and interpreted differently
  -- on server and on each client. Therefore, the type is a view on the
  -- game state, not the real game state, except on the server that
  -- alone stores the full game information.
  data State = State
    { _sdungeon        :: Dungeon    -- ^ remembered dungeon
    , _stotalDepth     :: Dice.AbsDepth
                                     -- ^ absolute dungeon depth for item creation
    , _sactorD         :: ActorDict  -- ^ remembered actors in the dungeon
    , _sitemD          :: ItemDict   -- ^ remembered items in the dungeon
    , _sitemIxMap      :: ItemIxMap  -- ^ spotted items with the same kind index
                                     --   could be recomputed at resume, but small
    , _sfactionD       :: FactionDict
                                     -- ^ remembered sides still in game
    , _stime           :: Time       -- ^ global game time, for UI display only
    , _scops           :: COps       -- ^ remembered content; warning: use only
                                     --   validated content, even for testing
    , _sgold           :: Int        -- ^ total value of human trinkets in dungeon
    , _shigh           :: HighScore.ScoreDict  -- ^ high score table
    , _sgameModeId     :: ContentId ModeKind   -- ^ current game mode
    , _sdiscoKind      :: DiscoveryKind        -- ^ item kind discoveries data
    , _sdiscoAspect    :: DiscoveryAspect
                                     -- ^ item aspect data; could be recomputed
    , _sactorMaxSkills :: ActorMaxSkills
                                     -- ^ actor maximal skills; is recomputed
    }
    deriving (Show, Eq)
  
  instance Binary State where
    put State{..} = do
      put _sdungeon
      put _stotalDepth
      put _sactorD
      put _sitemD
      put _sitemIxMap
      put _sfactionD
      put _stime
      put _sgold
      put _shigh
      put _sgameModeId
      put _sdiscoKind
      put _sdiscoAspect
    get = do
      _sdungeon <- get
      _stotalDepth <- get
      _sactorD <- get
      _sitemD <- get
      _sitemIxMap <- get
      _sfactionD <- get
      _stime <- get
      _sgold <- get
      _shigh <- get
      _sgameModeId <- get
      _sdiscoKind <- get
      _sdiscoAspect <- get
      let _scops = emptyCOps
          _sactorMaxSkills = EM.empty
      return $! State{..}
  
  sdungeon :: State -> Dungeon
  sdungeon = _sdungeon
  
  stotalDepth :: State -> Dice.AbsDepth
  stotalDepth = _stotalDepth
  
  sactorD :: State -> ActorDict
  sactorD = _sactorD
  
  sitemD :: State -> ItemDict
  sitemD = _sitemD
  
  sitemIxMap :: State -> ItemIxMap
  sitemIxMap = _sitemIxMap
  
  sfactionD :: State -> FactionDict
  sfactionD = _sfactionD
  
  stime :: State -> Time
  stime = _stime
  
  scops :: State -> COps
  scops = _scops
  
  sgold :: State -> Int
  sgold = _sgold
  
  shigh :: State -> HighScore.ScoreDict
  shigh = _shigh
  
  sgameModeId :: State -> ContentId ModeKind
  sgameModeId = _sgameModeId
  
  sdiscoKind :: State -> DiscoveryKind
  sdiscoKind = _sdiscoKind
  
  sdiscoAspect :: State -> DiscoveryAspect
  sdiscoAspect = _sdiscoAspect
  
  sactorMaxSkills :: State -> ActorMaxSkills
  sactorMaxSkills = _sactorMaxSkills
  
  unknownLevel :: COps -> ContentId CaveKind -> Dice.AbsDepth -> Area
               -> ([Point], [Point]) -> [Point] -> Int -> Bool
               -> Level
  unknownLevel COps{corule, cotile}
               lkind ldepth larea lstair lescape lexpl lnight =
    let outerId = ouniqGroup cotile TK.S_UNKNOWN_OUTER_FENCE
    in Level { lkind
             , ldepth
             , lfloor = EM.empty
             , lembed = EM.empty
             , lbig = EM.empty
             , lproj = EM.empty
             , ltile = unknownTileMap larea outerId (rWidthMax corule) (rHeightMax corule)
             , lentry = EM.empty
             , larea
             , lsmell = EM.empty
             , lstair
             , lescape
             , lseen = 0
             , lexpl
             , ltime = timeZero
             , lnight
             }
  
  -- | Create a map full of unknown tiles.
  --
  -- >>> unknownTileMap (fromJust (toArea (0,0,0,0))) TK.unknownId 2 2
  -- PointArray.Array with size (2,2)
  unknownTileMap :: Area -> ContentId TileKind -> X -> Y -> TileMap
  unknownTileMap larea outerId rWidthMax rHeightMax =
    let unknownMap = PointArray.replicateA rWidthMax rHeightMax TK.unknownId
        outerUpdate = map (, outerId) (areaInnerBorder larea)
    in unknownMap PointArray.// outerUpdate
  
  -- | Initial complete global game state.
  defStateGlobal :: Dungeon -> Dice.AbsDepth -> FactionDict -> COps
                 -> HighScore.ScoreDict -> ContentId ModeKind -> DiscoveryKind
                 -> State
  defStateGlobal _sdungeon _stotalDepth _sfactionD _scops _shigh _sgameModeId
                 _sdiscoKind =
    State
      { _sactorD = EM.empty
      , _sitemD = EM.empty
      , _sitemIxMap = EM.empty
      , _stime = timeZero
      , _sgold = 0
      , _sdiscoAspect = EM.empty
      , _sactorMaxSkills = EM.empty
      , ..
      }
  
  -- | Initial empty state.
  emptyState :: State
  emptyState =
    State
      { _sdungeon = EM.empty
      , _stotalDepth = Dice.AbsDepth 0
      , _sactorD = EM.empty
      , _sitemD = EM.empty
      , _sitemIxMap = EM.empty
      , _sfactionD = EM.empty
      , _stime = timeZero
      , _scops = emptyCOps
      , _sgold = 0
      , _shigh = HighScore.empty
      , _sgameModeId = toEnum 0  -- the initial value is unused
      , _sdiscoKind = EM.empty
      , _sdiscoAspect = EM.empty
      , _sactorMaxSkills = EM.empty
      }
  
  -- | Local state created by removing secret information from global
  -- state components.
  localFromGlobal :: State -> State
  localFromGlobal State{..} =
    State
      { _sdungeon =
        EM.map (\Level{..} ->
                unknownLevel _scops lkind ldepth larea
                             lstair lescape lexpl lnight)
               _sdungeon
      , ..
      }
  
  -- | Update dungeon data within state.
  updateDungeon :: (Dungeon -> Dungeon) -> State -> State
  updateDungeon f s = s {_sdungeon = f (_sdungeon s)}
  
  -- | Update dungeon depth.
  updateDepth :: (Dice.AbsDepth -> Dice.AbsDepth) -> State -> State
  updateDepth f s = s {_stotalDepth = f (_stotalDepth s)}
  
  -- | Update the actor dictionary.
  updateActorD :: (ActorDict -> ActorDict) -> State -> State
  {-# INLINE updateActorD #-}  -- just in case inliner goes hiwire
  updateActorD f s = s {_sactorD = f (_sactorD s)}
  
  -- | Update the item dictionary.
  updateItemD :: (ItemDict -> ItemDict) -> State -> State
  {-# INLINE updateItemD #-}
  updateItemD f s = s {_sitemD = f (_sitemD s)}
  
  -- | Update the item kind index map.
  updateItemIxMap :: (ItemIxMap -> ItemIxMap) -> State -> State
  updateItemIxMap f s = s {_sitemIxMap = f (_sitemIxMap s)}
  
  -- | Update faction data within state.
  updateFactionD :: (FactionDict -> FactionDict) -> State -> State
  updateFactionD f s = s {_sfactionD = f (_sfactionD s)}
  
  -- | Update global time within state.
  updateTime :: (Time -> Time) -> State -> State
  {-# INLINE updateTime #-}
  updateTime f s = s {_stime = f (_stime s)}
  
  -- | Update content data within state and recompute the cached data.
  updateCOpsAndCachedData :: (COps -> COps) -> State -> State
  updateCOpsAndCachedData f s =
    let s2 = s {_scops = f (_scops s)}
    in s2 {_sactorMaxSkills = maxSkillsInDungeon s2}
  
  -- | Update total gold value in the dungeon.
  updateGold :: (Int -> Int) -> State -> State
  updateGold f s = s {_sgold = f (_sgold s)}
  
  updateDiscoKind :: (DiscoveryKind -> DiscoveryKind) -> State -> State
  updateDiscoKind f s = s {_sdiscoKind = f (_sdiscoKind s)}
  
  updateDiscoAspect :: (DiscoveryAspect -> DiscoveryAspect) -> State -> State
  updateDiscoAspect f s = s {_sdiscoAspect = f (_sdiscoAspect s)}
  
  updateActorMaxSkills :: (ActorMaxSkills -> ActorMaxSkills) -> State -> State
  updateActorMaxSkills f s = s {_sactorMaxSkills = f (_sactorMaxSkills s)}
  
  getItemBody :: ItemId -> State -> Item
  getItemBody iid s = sitemD s EM.! iid
  
  -- This is best guess, including mean aspect record, so we can take into
  -- consideration even the kind the item hides under.
  aspectRecordFromItem :: ItemId -> Item -> State -> IA.AspectRecord
  aspectRecordFromItem iid item s =
    let kindId = case jkind item of
          IdentityObvious ik -> ik
          IdentityCovered ix ik -> fromMaybe ik $ ix `EM.lookup` sdiscoKind s
        COps{coItemSpeedup} = scops s
        mean = IA.kmMean $ getKindMean kindId coItemSpeedup
    in fromMaybe mean $ EM.lookup iid $ sdiscoAspect s
  
  aspectRecordFromIid :: ItemId -> State -> IA.AspectRecord
  aspectRecordFromIid iid s = aspectRecordFromItem iid (getItemBody iid s) s
  
  maxSkillsFromActor :: Actor -> State -> Ability.Skills
  maxSkillsFromActor b s =
    let processIid (iid, (k, _)) = (IA.aSkills $ aspectRecordFromIid iid s, k)
        processBag sks = Ability.sumScaledSkills $ map processIid sks
    in processBag $ EM.assocs (borgan b) ++ EM.assocs (beqp b)
  
  maxSkillsInDungeon :: State -> ActorMaxSkills
  maxSkillsInDungeon s =
    EM.map (`maxSkillsFromActor` s) $ sactorD s
  