content: {-# 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, 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.SplitMix32 as SM
  
  import           Game.LambdaHack.Common.Point
  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.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
    , aPresentAs :: 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)
  
  emptyAspectRecord :: AspectRecord
  emptyAspectRecord = AspectRecord
    { aTimeout = 0
    , aSkills = Ability.zeroSkills
    , aFlags = Ability.Flags ES.empty
    , aELabel = ""
    , aToThrow = IK.ThrowMod 100 100 1
    , aPresentAs = 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.PresentAs ha -> return $! ar {aPresentAs = 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 = SM.mkSMGen 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.PresentAs ha -> ar {aPresentAs = 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 | aToThrow /= IK.ThrowMod 100 100 1]
    ++ maybe [] (\ha -> [IK.PresentAs ha]) aPresentAs
    ++ 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
    || all IK.alwaysDudEffect (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 IK.VALUABLE $ IK.ifreq itemKind
      -- risk from treasure hunters
  
  goesIntoEqp :: AspectRecord -> Bool
  goesIntoEqp ar = checkFlag Ability.Equipable ar
                   || checkFlag Ability.Meleeable ar
  
  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
  