-- | The monad for writing to the main game state.
module Game.LambdaHack.Atomic.MonadStateWrite
  ( MonadStateWrite(..), AtomicFail(..), atomicFail
  , updateLevel, updateActor, updateFaction
  , moveActorMap, swapActorMap
  , insertBagContainer, insertItemContainer, insertItemActor
  , deleteBagContainer, deleteItemContainer, deleteItemActor
  , itemsMatch, addItemToActorMaxSkills, resetActorMaxSkills
#ifdef EXPOSE_INTERNAL
    -- * Internal operations
  , insertItemFloor, insertItemEmbed
  , insertItemOrgan, insertItemEqp, insertItemStash
  , deleteItemFloor, deleteItemEmbed
  , deleteItemOrgan, deleteItemEqp, deleteItemStash
  , rmFromBag
#endif
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import qualified Control.Exception as Ex
import qualified Data.EnumMap.Strict as EM
import           Data.Key (mapWithKeyM_)

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.Level
import           Game.LambdaHack.Common.MonadStateRead
import           Game.LambdaHack.Common.Point
import           Game.LambdaHack.Common.State
import           Game.LambdaHack.Common.Types
import qualified Game.LambdaHack.Definition.Ability as Ability
import           Game.LambdaHack.Definition.Defs

-- | The monad for writing to the main game state. Atomic updates (@UpdAtomic@)
-- are given semantics in this monad.
class MonadStateRead m => MonadStateWrite m where
  modifyState :: (State -> State) -> m ()
  putState :: State -> m ()

-- | Exception signifying that atomic action failed because
-- the information it carries is inconsistent with the client's state,
-- (e.g., because the client knows too little to understand the command
-- or already deduced the state change from earlier commands
-- or is confused, amnesiac or sees illusory actors or tiles).
-- Whenever we know the failure is logically impossible,
-- we don't throw the @AtomicFail@ exception, but insert a normal assertion
-- or @error@ call, which are never caught nor handled.
newtype AtomicFail = AtomicFail String
  deriving Show

instance Ex.Exception AtomicFail

atomicFail :: String -> a
atomicFail = Ex.throw . AtomicFail

-- INLIning offers no speedup, increases alloc and binary size.
-- EM.alter not necessary, because levels not removed, so little risk
-- of adjusting at absent index.
updateLevel :: MonadStateWrite m => LevelId -> (Level -> Level) -> m ()
updateLevel lid f = modifyState $ updateDungeon $ EM.adjust f lid

-- INLIning doesn't help despite probably canceling the alt indirection.
-- perhaps it's applied automatically.
updateActor :: MonadStateWrite m => ActorId -> (Actor -> Actor) -> m ()
updateActor aid f = do
  let alt Nothing = error $ "no body to update" `showFailure` aid
      alt (Just b) = Just $ f b
  modifyState $ updateActorD $ EM.alter alt aid

updateFaction :: MonadStateWrite m => FactionId -> (Faction -> Faction) -> m ()
updateFaction fid f = do
  let alt Nothing = error $ "no faction to update" `showFailure` fid
      alt (Just fact) = Just $ f fact
  modifyState $ updateFactionD $ EM.alter alt fid

moveActorMap :: MonadStateWrite m => ActorId -> Actor -> Actor -> m ()
moveActorMap aid body newBody = do
  let rmBig Nothing = error $ "actor already removed"
                              `showFailure` (aid, body)
      rmBig (Just _aid2) =
#ifdef WITH_EXPENSIVE_ASSERTIONS
        assert (aid == _aid2 `blame` "actor already removed"
                             `swith` (aid, body, _aid2))
#endif
        Nothing
      addBig Nothing = Just aid
      addBig (Just aid2) = error $ "an actor already present there"
                                   `showFailure` (aid, body, aid2)
      updBig = EM.alter addBig (bpos newBody)
               . EM.alter rmBig (bpos body)
  let rmProj Nothing = error $ "actor already removed"
                               `showFailure` (aid, body)
      rmProj (Just l) =
#ifdef WITH_EXPENSIVE_ASSERTIONS
        assert (aid `elem` l `blame` "actor already removed"
                             `swith` (aid, body, l))
#endif
        (let l2 = delete aid l
         in if null l2 then Nothing else Just l2)
      addProj Nothing = Just [aid]
      addProj (Just l) = Just $ aid : l
      updProj = EM.alter addProj (bpos newBody)
                . EM.alter rmProj (bpos body)
  updateLevel (blid body) $ if bproj body
                            then updateProjMap updProj
                            else updateBigMap updBig

swapActorMap :: MonadStateWrite m
             => ActorId -> Actor -> ActorId -> Actor -> m ()
swapActorMap source sbody target tbody = do
  let addBig aid1 aid2 Nothing =
        error $ "actor already removed"
                `showFailure` (aid1, aid2, source, sbody, target, tbody)
      addBig _aid1 aid2 (Just _aid) =
#ifdef WITH_EXPENSIVE_ASSERTIONS
        assert (_aid == _aid1 `blame` "wrong actor present"
                              `swith` (_aid, _aid1, aid2, sbody, tbody))
#endif
        (Just aid2)
      updBig = EM.alter (addBig source target) (bpos sbody)
               . EM.alter (addBig target source) (bpos tbody)
  if not (bproj sbody) && not (bproj tbody)
  then updateLevel (blid sbody) $ updateBigMap updBig
  else do
    moveActorMap source sbody tbody
    moveActorMap target tbody sbody

insertBagContainer :: MonadStateWrite m
                   => ItemBag -> Container -> m ()
insertBagContainer bag c = case c of
  CFloor lid pos -> do
    let alt Nothing = Just bag
        alt (Just bag2) = atomicFail $ "floor bag not empty"
                                       `showFailure` (bag2, lid, pos, bag)
    updateLevel lid $ updateFloor $ EM.alter alt pos
  CEmbed lid pos -> do
    let alt Nothing = Just bag
        alt (Just bag2) = atomicFail $ "embed bag not empty"
                                       `showFailure` (bag2, lid, pos, bag)
    updateLevel lid $ updateEmbed $ EM.alter alt pos
  CActor aid store ->
    -- Very unlikely case, so we prefer brevity over performance.
    mapWithKeyM_ (\iid kit -> insertItemActor iid kit aid store) bag
  CTrunk{} -> return ()

insertItemContainer :: MonadStateWrite m
                    => ItemId -> ItemQuant -> Container -> m ()
insertItemContainer iid kit c = case c of
  CFloor lid pos -> insertItemFloor iid kit lid pos
  CEmbed lid pos -> insertItemEmbed iid kit lid pos
  CActor aid store -> insertItemActor iid kit aid store
  CTrunk{} -> return ()

-- New @kit@ lands at the front of the list.
insertItemFloor :: MonadStateWrite m
                => ItemId -> ItemQuant -> LevelId -> Point -> m ()
insertItemFloor iid kit lid pos =
  let bag = EM.singleton iid kit
      mergeBag = EM.insertWith (EM.unionWith mergeItemQuant) pos bag
  in updateLevel lid $ updateFloor mergeBag

insertItemEmbed :: MonadStateWrite m
                => ItemId -> ItemQuant -> LevelId -> Point -> m ()
insertItemEmbed iid kit lid pos =
  let bag = EM.singleton iid kit
      mergeBag = EM.insertWith (EM.unionWith mergeItemQuant) pos bag
  in updateLevel lid $ updateEmbed mergeBag

insertItemActor :: MonadStateWrite m
                => ItemId -> ItemQuant -> ActorId -> CStore -> m ()
insertItemActor iid kit aid cstore = case cstore of
  CGround -> do
    b <- getsState $ getActorBody aid
    insertItemFloor iid kit (blid b) (bpos b)
  COrgan -> insertItemOrgan iid kit aid
  CEqp -> insertItemEqp iid kit aid
  CStash -> do
    b <- getsState $ getActorBody aid
    insertItemStash iid kit (bfid b)

-- We assume @Meleeable@ and @Benign@ are never secret and so we don't need
-- to fix the weapon counts when the item is identified later on.
insertItemOrgan :: MonadStateWrite m => ItemId -> ItemQuant -> ActorId -> m ()
insertItemOrgan iid kit@(k, _) aid = do
  arItem <- getsState $ aspectRecordFromIid iid
  let bag = EM.singleton iid kit
      upd = EM.unionWith mergeItemQuant bag
  updateActor aid $ \b ->
    b { borgan = upd (borgan b)
      , bweapon = if IA.checkFlag Ability.Meleeable arItem
                  then bweapon b + k
                  else bweapon b
      , bweapBenign = if IA.checkFlag Ability.Meleeable arItem
                         && IA.checkFlag Ability.Benign arItem
                      then bweapBenign b + k
                      else bweapBenign b }

insertItemEqp :: MonadStateWrite m => ItemId -> ItemQuant -> ActorId -> m ()
insertItemEqp iid kit@(k, _) aid = do
  arItem <- getsState $ aspectRecordFromIid iid
  let bag = EM.singleton iid kit
      upd = EM.unionWith mergeItemQuant bag
  updateActor aid $ \b ->
    b { beqp = upd (beqp b)
      , bweapon = if IA.checkFlag Ability.Meleeable arItem
                  then bweapon b + k
                  else bweapon b
      , bweapBenign = if IA.checkFlag Ability.Meleeable arItem
                         && IA.checkFlag Ability.Benign arItem
                      then bweapBenign b + k
                      else bweapBenign b }

insertItemStash :: MonadStateWrite m => ItemId -> ItemQuant -> FactionId -> m ()
insertItemStash iid kit fid = do
  mstash <- getsState $ \s -> gstash $ sfactionD s EM.! fid
  case mstash of
    Just (lid, pos) -> insertItemFloor iid kit lid pos
      -- can't be inserted into outdated or unseen stash position,
      -- because such commands are visible only when the stash position is
      -- and so @gstash@ points at the correct one, thanks to @atomicRemember@
    Nothing -> error $ "" `showFailure` (iid, kit, fid)

deleteBagContainer :: MonadStateWrite m
                   => ItemBag -> Container -> m ()
deleteBagContainer bag c = case c of
  CFloor lid pos -> do
    let alt Nothing = atomicFail $ "floor bag already empty"
                                   `showFailure` (lid, pos, bag)
        alt (Just bag2) = assert (bag == bag2) Nothing
    updateLevel lid $ updateFloor $ EM.alter alt pos
  CEmbed lid pos -> do
    let alt Nothing = atomicFail $ "embed bag already empty"
                                   `showFailure` (lid, pos, bag)
        alt (Just bag2) = assert (bag == bag2 `blame` (bag, bag2)) Nothing
    updateLevel lid $ updateEmbed $ EM.alter alt pos
  CActor aid store ->
    -- Very unlikely case, so we prefer brevity over performance.
    mapWithKeyM_ (\iid kit -> deleteItemActor iid kit aid store) bag
  CTrunk{} -> error $ "" `showFailure` c

deleteItemContainer :: MonadStateWrite m
                    => ItemId -> ItemQuant -> Container -> m ()
deleteItemContainer iid kit c = case c of
  CFloor lid pos -> deleteItemFloor iid kit lid pos
  CEmbed lid pos -> deleteItemEmbed iid kit lid pos
  CActor aid store -> deleteItemActor iid kit aid store
  CTrunk{} -> error $ "" `showFailure` c

deleteItemFloor :: MonadStateWrite m
                => ItemId -> ItemQuant -> LevelId -> Point -> m ()
deleteItemFloor iid kit lid pos =
  let rmFromFloor (Just bag) =
        let nbag = rmFromBag kit iid bag
        in if EM.null nbag then Nothing else Just nbag
      rmFromFloor Nothing = error $ "item already removed"
                                    `showFailure` (iid, kit, lid, pos)
  in updateLevel lid $ updateFloor $ EM.alter rmFromFloor pos

deleteItemEmbed :: MonadStateWrite m
                => ItemId -> ItemQuant -> LevelId -> Point -> m ()
deleteItemEmbed iid kit lid pos =
  let rmFromFloor (Just bag) =
        let nbag = rmFromBag kit iid bag
        in if EM.null nbag then Nothing else Just nbag
      rmFromFloor Nothing = error $ "item already removed"
                                    `showFailure` (iid, kit, lid, pos)
  in updateLevel lid $ updateEmbed $ EM.alter rmFromFloor pos

deleteItemActor :: MonadStateWrite m
                => ItemId -> ItemQuant -> ActorId -> CStore -> m ()
deleteItemActor iid kit aid cstore = case cstore of
  CGround -> do
    b <- getsState $ getActorBody aid
    deleteItemFloor iid kit (blid b) (bpos b)
  COrgan -> deleteItemOrgan iid kit aid
  CEqp -> deleteItemEqp iid kit aid
  CStash -> do
    b <- getsState $ getActorBody aid
    deleteItemStash iid kit (bfid b)

deleteItemOrgan :: MonadStateWrite m => ItemId -> ItemQuant -> ActorId -> m ()
deleteItemOrgan iid kit@(k, _) aid = do
  arItem <- getsState $ aspectRecordFromIid iid
  updateActor aid $ \b ->
    b { borgan = rmFromBag kit iid (borgan b)
      , bweapon = if IA.checkFlag Ability.Meleeable arItem
                  then bweapon b - k
                  else bweapon b
      , bweapBenign = if IA.checkFlag Ability.Meleeable arItem
                         && IA.checkFlag Ability.Benign arItem
                      then bweapBenign b - k
                      else bweapBenign b }

deleteItemEqp :: MonadStateWrite m => ItemId -> ItemQuant -> ActorId -> m ()
deleteItemEqp iid kit@(k, _) aid = do
  arItem <- getsState $ aspectRecordFromIid iid
  updateActor aid $ \b ->
    b { beqp = rmFromBag kit iid (beqp b)
      , bweapon = if IA.checkFlag Ability.Meleeable arItem
                  then bweapon b - k
                  else bweapon b
      , bweapBenign = if IA.checkFlag Ability.Meleeable arItem
                         && IA.checkFlag Ability.Benign arItem
                      then bweapBenign b - k
                      else bweapBenign b }

deleteItemStash :: MonadStateWrite m => ItemId -> ItemQuant -> FactionId -> m ()
deleteItemStash iid kit fid = do
  mstash <- getsState $ \s -> gstash $ sfactionD s EM.! fid
  case mstash of
    Just (lid, pos) -> deleteItemFloor iid kit lid pos
      -- can't be deleted from an outdated or unseen stash position,
      -- because such commands are visible only when the stash position is
      -- and so @gstash@ points at the correct one, thanks to @atomicRemember@
    Nothing -> error $ "" `showFailure` (iid, kit, fid)

-- Removing the part of the kit from the back of the list,
-- so that @DestroyItem kit (CreateItem kit x) == x@.
rmFromBag :: ItemQuant -> ItemId -> ItemBag -> ItemBag
rmFromBag kit@(k, rmIt) iid bag =
  let rfb Nothing = error $ "rm from empty slot" `showFailure` (k, iid, bag)
      rfb (Just (n, it)) =
        case compare n k of
          LT -> error $ "rm more than there is"
                        `showFailure` (n, kit, iid, bag)
          EQ -> assert (rmIt == it `blame` (rmIt, it, n, kit, iid, bag)) Nothing
          GT -> assert (rmIt == take k it
                        `blame` (rmIt, take k it, n, kit, iid, bag))
                $ Just (n - k, take (n - k) it)
  in EM.alter rfb iid bag

itemsMatch :: Item -> Item -> Bool
itemsMatch item1 item2 =
  jkind item1 == jkind item2
  -- Note that nothing else needs to be the same, since items are merged
  -- and clients have different views on dungeon items than the server.

addItemToActorMaxSkills :: MonadStateWrite m
                        => ItemId -> Item -> Int -> ActorId -> m ()
addItemToActorMaxSkills iid itemBase k aid = do
  arItem <- getsState $ aspectRecordFromItem iid itemBase
  let f actorMaxSk =
        Ability.sumScaledSkills [(actorMaxSk, 1), (IA.aSkills arItem, k)]
  modifyState $ updateActorMaxSkills $ EM.adjust f aid

resetActorMaxSkills :: MonadStateWrite m => m ()
resetActorMaxSkills = do
  -- Each actor's equipment and organs would need to be inspected,
  -- the iid looked up, e.g., if it wasn't in old discoKind, but is in new,
  -- and then aspect record updated, so it's simpler and not much more
  -- expensive to generate new sactorMaxSkills. Optimize only after profiling.
  -- Also note this doesn't get invoked on the server, because it bails out
  -- earlier, upon noticing the item is already fully known.
  actorMaxSk <- getsState maxSkillsInDungeon
  modifyState $ updateActorMaxSkills $ const actorMaxSk
