-- | Server and client game state types and operations.
module Game.LambdaHack.Common.State
  ( -- * Basic game state, local or global
    State
    -- * State components
  , sdungeon, stotalDepth, sactorD, sitemD, sfactionD, stime, scops, shigh
    -- * State operations
  , defStateGlobal, emptyState, localFromGlobal
  , updateDungeon, updateDepth, updateActorD, updateItemD
  , updateFactionD, updateTime, updateCOps
  ) where

import Data.Binary
import qualified Data.EnumMap.Strict as EM
import Data.Text (Text)

import Game.LambdaHack.Common.Actor
import Game.LambdaHack.Common.Faction
import qualified Game.LambdaHack.Common.HighScore as HighScore
import Game.LambdaHack.Common.Item
import qualified Game.LambdaHack.Common.Kind as Kind
import Game.LambdaHack.Common.Level
import Game.LambdaHack.Common.Misc
import Game.LambdaHack.Common.Point
import qualified Game.LambdaHack.Common.PointArray as PointArray
import Game.LambdaHack.Common.Time
import Game.LambdaHack.Content.TileKind

-- | View on game state. "Remembered" fields carry a subset of the info
-- in the client copies of the state. Clients never directly change
-- their @State@, but apply atomic actions sent by the server to do so.
data State = State
  { _sdungeon    :: !Dungeon      -- ^ remembered dungeon
  , _stotalDepth :: !AbsDepth     -- ^ absolute dungeon depth, for item creation
  , _sactorD     :: !ActorDict    -- ^ remembered actors in the dungeon
  , _sitemD      :: !ItemDict     -- ^ remembered items in the dungeon
  , _sfactionD   :: !FactionDict  -- ^ remembered sides still in game
  , _stime       :: !Time         -- ^ global game time
  , _scops       :: Kind.COps     -- ^ remembered content
  , _shigh       :: !HighScore.ScoreTable  -- ^ high score table
  }
  deriving (Show, Eq)

-- TODO: add a flag 'fresh' and when saving levels, don't save
-- and when loading regenerate this level.
unknownLevel :: Kind.COps -> AbsDepth -> X -> Y
             -> Text -> ([Point], [Point]) -> Int -> Int -> Int -> Bool
             -> Level
unknownLevel Kind.COps{cotile=Kind.Ops{ouniqGroup}}
             ldepth lxsize lysize ldesc lstair lclear
             lsecret lhidden lescape =
  let unknownId = ouniqGroup "unknown space"
      outerId = ouniqGroup "basic outer fence"
  in Level { ldepth
           , lprio = EM.empty
           , lfloor = EM.empty
           , ltile = unknownTileMap unknownId outerId lxsize lysize
           , lxsize
           , lysize
           , lsmell = EM.empty
           , ldesc
           , lstair
           , lseen = 0
           , lclear
           , ltime = timeZero
           , lactorCoeff = 0
           , lactorFreq = []
           , litemNum = 0
           , litemFreq = []
           , lsecret
           , lhidden
           , lescape
           }

unknownTileMap :: Kind.Id TileKind -> Kind.Id TileKind -> Int -> Int -> TileMap
unknownTileMap unknownId outerId lxsize lysize =
  let unknownMap = PointArray.replicateA lxsize lysize unknownId
      borders = [ Point x y
                | x <- [0, lxsize - 1], y <- [1..lysize - 2] ]
                ++ [ Point x y
                   | x <- [0..lxsize - 1], y <- [0, lysize - 1] ]
      outerUpdate = zip borders $ repeat outerId
  in unknownMap PointArray.// outerUpdate

-- | Initial complete global game state.
defStateGlobal :: Dungeon -> AbsDepth
               -> FactionDict -> Kind.COps -> HighScore.ScoreTable
               -> State
defStateGlobal _sdungeon _stotalDepth _sfactionD _scops _shigh =
  State
    { _sactorD = EM.empty
    , _sitemD = EM.empty
    , _stime = timeZero
    , ..
    }

-- | Initial empty state.
emptyState :: State
emptyState =
  State
    { _sdungeon = EM.empty
    , _stotalDepth = AbsDepth 0
    , _sactorD = EM.empty
    , _sitemD = EM.empty
    , _sfactionD = EM.empty
    , _stime = timeZero
    , _scops = undefined
    , _shigh = HighScore.empty
    }

-- TODO: make lstair secret until discovered; use this later on for
-- goUp in targeting mode (land on stairs of on the same location up a level
-- if this set of stsirs is unknown).
-- | Local state created by removing secret information from global
-- state components.
localFromGlobal :: State -> State
localFromGlobal State{..} =
  State
    { _sdungeon =
      EM.map (\Level{..} ->
              unknownLevel _scops ldepth lxsize lysize ldesc lstair lclear
                           lsecret lhidden lescape)
            _sdungeon
    , ..
    }

-- | Update dungeon data within state.
updateDungeon :: (Dungeon -> Dungeon) -> State -> State
updateDungeon f s = s {_sdungeon = f (_sdungeon s)}

-- | Update dungeon depth.
updateDepth :: (AbsDepth -> AbsDepth) -> State -> State
updateDepth f s = s {_stotalDepth = f (_stotalDepth s)}

-- | Update the actor dictionary.
updateActorD :: (ActorDict -> ActorDict) -> State -> State
updateActorD f s = s {_sactorD = f (_sactorD s)}

-- | Update the item dictionary.
updateItemD :: (ItemDict -> ItemDict) -> State -> State
updateItemD f s = s {_sitemD = f (_sitemD 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
updateTime f s = s {_stime = f (_stime s)}

-- | Update content data within state.
updateCOps :: (Kind.COps -> Kind.COps) -> State -> State
updateCOps f s = s {_scops = f (_scops s)}

sdungeon :: State -> Dungeon
sdungeon = _sdungeon

stotalDepth :: State -> AbsDepth
stotalDepth = _stotalDepth

sactorD :: State -> ActorDict
sactorD = _sactorD

sitemD :: State -> ItemDict
sitemD = _sitemD

sfactionD :: State -> FactionDict
sfactionD = _sfactionD

stime :: State -> Time
stime = _stime

scops :: State -> Kind.COps
scops = _scops

shigh :: State -> HighScore.ScoreTable
shigh = _shigh

instance Binary State where
  put State{..} = do
    put _sdungeon
    put _stotalDepth
    put _sactorD
    put _sitemD
    put _sfactionD
    put _stime
    put _shigh
  get = do
    _sdungeon <- get
    _stotalDepth <- get
    _sactorD <- get
    _sitemD <- get
    _sfactionD <- get
    _stime <- get
    _shigh <- get
    let _scops = undefined  -- overwritten by recreated cops
    return $! State{..}