{-# LANGUAGE DeriveGeneric #-}
-- | The type of item aspects and its operations.
module Game.LambdaHack.Common.ItemAspect
  ( AspectRecord(..), KindMean(..)
  , emptyAspectRecord, addMeanAspect, castAspect, aspectsRandom
  , aspectRecordToList, rollAspectRecord, getSkill, checkFlag, meanAspect
  , onlyMinorEffects, itemTrajectory, totalRange, isHumanTrinket
  , goesIntoEqp, goesIntoInv, goesIntoSha, loreFromMode, loreFromContainer
#ifdef EXPOSE_INTERNAL
    -- * Internal operations
  , ceilingMeanDice
#endif
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import qualified Control.Monad.Trans.State.Strict as St
import           Data.Binary
import qualified Data.EnumSet as ES
import           Data.Hashable (Hashable)
import qualified Data.Text as T
import           GHC.Generics (Generic)
import qualified System.Random as R

import           Game.LambdaHack.Common.Time
import           Game.LambdaHack.Common.Types
import           Game.LambdaHack.Common.Vector
import qualified Game.LambdaHack.Content.ItemKind as IK
import qualified Game.LambdaHack.Core.Dice as Dice
import           Game.LambdaHack.Common.Point
import           Game.LambdaHack.Core.Random
import qualified Game.LambdaHack.Definition.Ability as Ability
import           Game.LambdaHack.Definition.Defs

-- | Record of skills conferred by an item as well as of item flags
-- and other item aspects.
data AspectRecord = AspectRecord
  { aTimeout :: Int
  , aSkills  :: Ability.Skills
  , aFlags   :: Ability.Flags
  , aELabel  :: Text
  , aToThrow :: IK.ThrowMod
  , aHideAs  :: Maybe (GroupName IK.ItemKind)
  , aEqpSlot :: Maybe Ability.EqpSlot
  }
  deriving (Show, Eq, Ord, Generic)

instance Hashable AspectRecord

instance Binary AspectRecord

-- | Partial information about an item, deduced from its item kind.
-- These are assigned to each 'IK.ItemKind'. The @kmConst@ flag says whether
-- the item's aspect record is constant rather than random or dependent
-- on item creation dungeon level.
data KindMean = KindMean
  { kmConst :: Bool  -- ^ whether the item doesn't need second identification
  , kmMean  :: AspectRecord  -- ^ mean value of item's possible aspect records
  }
  deriving (Show, Eq, Ord, Generic)

emptyAspectRecord :: AspectRecord
emptyAspectRecord = AspectRecord
  { aTimeout = 0
  , aSkills  = Ability.zeroSkills
  , aFlags   = Ability.Flags ES.empty
  , aELabel  = ""
  , aToThrow = IK.ThrowMod 100 100 1
  , aHideAs  = Nothing
  , aEqpSlot = Nothing
  }

castAspect :: Dice.AbsDepth -> Dice.AbsDepth -> AspectRecord -> IK.Aspect
           -> Rnd AspectRecord
castAspect !ldepth !totalDepth !ar !asp =
  case asp of
    IK.Timeout d -> do
      n <- castDice ldepth totalDepth d
      return $! assert (aTimeout ar == 0) $ ar {aTimeout = n}
    IK.AddSkill sk d -> do
      n <- castDice ldepth totalDepth d
      return $! if n /= 0
                then ar {aSkills = Ability.addSk sk n (aSkills ar)}
                else ar
    IK.SetFlag feat ->
      return $! ar {aFlags = Ability.Flags
                             $ ES.insert feat (Ability.flags $ aFlags ar)}
    IK.ELabel t -> return $! ar {aELabel = t}
    IK.ToThrow tt -> return $! ar {aToThrow = tt}
    IK.HideAs ha -> return $! ar {aHideAs = Just ha}
    IK.EqpSlot slot -> return $! ar {aEqpSlot = Just slot}
    IK.Odds d aspects1 aspects2 -> do
      pick1 <- oddsDice ldepth totalDepth d
      foldlM' (castAspect ldepth totalDepth) ar $
        if pick1 then aspects1 else aspects2

-- If @False@, aspects of this kind are most probably fixed, not random
-- nor dependent on dungeon level where the item is created.
aspectsRandom :: [IK.Aspect] -> Bool
aspectsRandom ass =
  let rollM depth =
        foldlM' (castAspect (Dice.AbsDepth depth) (Dice.AbsDepth 10))
                emptyAspectRecord ass
      gen = R.mkStdGen 0
      (ar0, gen0) = St.runState (rollM 0) gen
      (ar1, gen1) = St.runState (rollM 10) gen0
  in show gen /= show gen0 || show gen /= show gen1 || ar0 /= ar1

addMeanAspect :: AspectRecord -> IK.Aspect -> AspectRecord
addMeanAspect !ar !asp =
  case asp of
    IK.Timeout d ->
      let n = ceilingMeanDice d
      in assert (aTimeout ar == 0) $ ar {aTimeout = n}
    IK.AddSkill sk d ->
      let n = ceilingMeanDice d
      in if n /= 0
         then ar {aSkills = Ability.addSk sk n (aSkills ar)}
         else ar
    IK.SetFlag feat ->
      ar {aFlags = Ability.Flags $ ES.insert feat (Ability.flags $ aFlags ar)}
    IK.ELabel t -> ar {aELabel = t}
    IK.ToThrow tt -> ar {aToThrow = tt}
    IK.HideAs ha -> ar {aHideAs = Just ha}
    IK.EqpSlot slot -> ar {aEqpSlot = Just slot}
    IK.Odds{} -> ar  -- can't tell, especially since we don't know the level

ceilingMeanDice :: Dice.Dice -> Int
ceilingMeanDice d = ceiling $ Dice.meanDice d

aspectRecordToList :: AspectRecord -> [IK.Aspect]
aspectRecordToList AspectRecord{..} =
  [IK.Timeout $ Dice.intToDice aTimeout | aTimeout /= 0]
  ++ [ IK.AddSkill sk $ Dice.intToDice n
     | (sk, n) <- Ability.skillsToList aSkills ]
  ++ [IK.SetFlag feat | feat <- ES.elems $ Ability.flags aFlags]
  ++ [IK.ELabel aELabel | not $ T.null aELabel]
  ++ [IK.ToThrow aToThrow | not $ aToThrow == IK.ThrowMod 100 100 1]
  ++ maybe [] (\ha -> [IK.HideAs ha]) aHideAs
  ++ maybe [] (\slot -> [IK.EqpSlot slot]) aEqpSlot

rollAspectRecord :: [IK.Aspect] -> Dice.AbsDepth -> Dice.AbsDepth
                 -> Rnd AspectRecord
rollAspectRecord ass ldepth totalDepth =
  foldlM' (castAspect ldepth totalDepth) emptyAspectRecord ass

getSkill :: Ability.Skill -> AspectRecord -> Int
{-# INLINE getSkill #-}
getSkill sk ar = Ability.getSk sk $ aSkills ar

checkFlag :: Ability.Flag -> AspectRecord -> Bool
{-# INLINE checkFlag #-}
checkFlag flag ar = Ability.checkFl flag (aFlags ar)

meanAspect :: IK.ItemKind -> AspectRecord
meanAspect kind = foldl' addMeanAspect emptyAspectRecord (IK.iaspects kind)

-- Kinetic damage is not considered major effect, even though it
-- identifies an item, when one hits with it. However, it's tedious
-- to wait for weapon identification until first hit and also
-- if a weapon is periodically activated, the kinetic damage would not apply,
-- so we'd need special cases that force identification or warn
-- or here not consider kinetic damage a major effect if item is periodic.
-- So we opt for KISS and identify effect-less weapons at pick-up,
-- not at first hit.
onlyMinorEffects :: AspectRecord -> IK.ItemKind -> Bool
onlyMinorEffects ar kind =
  checkFlag Ability.MinorEffects ar  -- override
  || not (any (not . IK.onSmashEffect) $ IK.ieffects kind)
       -- exhibits no major effects

itemTrajectory :: AspectRecord -> IK.ItemKind -> [Point]
               -> ([Vector], (Speed, Int))
itemTrajectory ar itemKind path =
  let IK.ThrowMod{..} = aToThrow ar
  in computeTrajectory (IK.iweight itemKind) throwVelocity throwLinger path

totalRange :: AspectRecord -> IK.ItemKind -> Int
totalRange ar itemKind = snd $ snd $ itemTrajectory ar itemKind []

isHumanTrinket :: IK.ItemKind -> Bool
isHumanTrinket itemKind =
  maybe False (> 0) $ lookup "valuable" $ IK.ifreq itemKind
    -- risk from treasure hunters

goesIntoEqp :: AspectRecord -> Bool
goesIntoEqp ar = checkFlag Ability.Equipable ar
                 || checkFlag Ability.Meleeable ar

goesIntoInv :: AspectRecord -> Bool
goesIntoInv ar = not (checkFlag Ability.Precious ar) && not (goesIntoEqp ar)

goesIntoSha :: AspectRecord -> Bool
goesIntoSha ar = checkFlag Ability.Precious ar && not (goesIntoEqp ar)

loreFromMode :: ItemDialogMode -> SLore
loreFromMode c = case c of
  MStore COrgan -> SOrgan
  MStore _ -> SItem
  MOrgans -> undefined  -- slots from many lore kinds
  MOwned -> SItem
  MSkills -> undefined  -- artificial slots
  MLore slore -> slore
  MPlaces -> undefined  -- artificial slots

loreFromContainer :: AspectRecord -> Container -> SLore
loreFromContainer arItem c = case c of
  CFloor{} -> SItem
  CEmbed{} -> SEmbed
  CActor _ store -> if | checkFlag Ability.Blast arItem -> SBlast
                       | checkFlag Ability.Condition arItem -> SCondition
                       | otherwise -> loreFromMode $ MStore store
  CTrunk{} -> if checkFlag Ability.Blast arItem then SBlast else STrunk