-- | Determining the strongest item wrt some property.
-- No operation in this module involves the state or any of our custom monads.
module Game.LambdaHack.Common.ItemStrongest
  ( -- * Strongest items
    strengthOnSmash, strengthToThrow, strengthEqpSlot, strengthFromEqpSlot
  , strongestSlotNoFilter, strongestSlot, sumSlotNoFilter, sumSkills
    -- * Assorted
  , totalRange, computeTrajectory, itemTrajectory
  , unknownPrecious, permittedRanged, unknownMelee
  ) where

import Control.Applicative
import Control.Exception.Assert.Sugar
import qualified Control.Monad.State as St
import qualified Data.EnumMap.Strict as EM
import Data.List
import Data.Maybe
import qualified Data.Ord as Ord
import Data.Text (Text)

import qualified Game.LambdaHack.Common.Ability as Ability
import qualified Game.LambdaHack.Common.Dice as Dice
import Game.LambdaHack.Common.Effect
import Game.LambdaHack.Common.Item
import Game.LambdaHack.Common.Point
import Game.LambdaHack.Common.Time
import Game.LambdaHack.Common.Vector
import Game.LambdaHack.Content.ItemKind

dice999 :: Dice.Dice -> Int
dice999 d = fromMaybe 999 $ Dice.reduceDice d

strengthAspect :: (Aspect Int -> [b]) -> ItemFull -> [b]
strengthAspect f itemFull =
  case itemDisco itemFull of
    Just ItemDisco{itemAE=Just ItemAspectEffect{jaspects}} ->
      concatMap f jaspects
    Just ItemDisco{itemKind=ItemKind{iaspects}} ->
      -- Approximation. For some effects lower values are better,
      -- so we can't put 999 here (and for summation, this is wrong).
      let trav x = St.evalState (aspectTrav x (return . round . Dice.meanDice))
                                ()
      in concatMap f $ map trav iaspects
    Nothing -> []

strengthAspectMaybe :: Show b => (Aspect Int -> [b]) -> ItemFull -> Maybe b
strengthAspectMaybe f itemFull =
  case strengthAspect f itemFull of
    [] -> Nothing
    [x] -> Just x
    xs -> assert `failure` (xs, itemFull)

strengthEffect999 :: (Effect Int -> [b]) -> ItemFull -> [b]
strengthEffect999 f itemFull =
  case itemDisco itemFull of
    Just ItemDisco{itemAE=Just ItemAspectEffect{jeffects}} ->
      concatMap f jeffects
    Just ItemDisco{itemKind=ItemKind{ieffects}} ->
      -- Default for unknown power is 999 to encourage experimenting.
      let trav x = St.evalState (effectTrav x (return . dice999)) ()
      in concatMap f $ map trav ieffects
    Nothing -> []

strengthFeature :: (Feature -> [b]) -> Item -> [b]
strengthFeature f item = concatMap f (jfeature item)

strengthMelee :: ItemFull -> Maybe Int
strengthMelee itemFull =
  let durable = Durable `elem` jfeature (itemBase itemFull)
      p (Hurt d) = [floor (Dice.meanDice d)]
      p (Burn k) = [k]
      p _ = []
      hasNoEffects = case itemDisco itemFull of
        Just ItemDisco{itemAE=Just ItemAspectEffect{jeffects}} ->
          null jeffects
        Just ItemDisco{itemKind=ItemKind{ieffects}} ->
          null ieffects
        Nothing -> True
  in if hasNoEffects
     then Nothing
     else Just $ sum (strengthEffect999 p itemFull)
                 + if durable then 100 else 0

-- Called only by the server, so 999 is OK.
strengthOnSmash :: ItemFull -> [Effect Int]
strengthOnSmash =
  let p (OnSmash eff) = [eff]
      p _ = []
  in strengthEffect999 p

strengthPeriodic :: ItemFull -> Maybe Int
strengthPeriodic =
  let p (Periodic k) = [k]
      p _ = []
  in strengthAspectMaybe p

strengthAddMaxHP :: ItemFull -> Maybe Int
strengthAddMaxHP =
  let p (AddMaxHP k) = [k]
      p _ = []
  in strengthAspectMaybe p

strengthAddMaxCalm :: ItemFull -> Maybe Int
strengthAddMaxCalm =
  let p (AddMaxCalm k) = [k]
      p _ = []
  in strengthAspectMaybe p

strengthAddSpeed :: ItemFull -> Maybe Int
strengthAddSpeed =
  let p (AddSpeed k) = [k]
      p _ = []
  in strengthAspectMaybe p

strengthAddSkills :: ItemFull -> Maybe Ability.Skills
strengthAddSkills =
  let p (AddSkills a) = [a]
      p _ = []
  in strengthAspectMaybe p

strengthAddHurtMelee :: ItemFull -> Maybe Int
strengthAddHurtMelee =
  let p (AddHurtMelee k) = [k]
      p _ = []
  in strengthAspectMaybe p

strengthAddHurtRanged :: ItemFull -> Maybe Int
strengthAddHurtRanged =
  let p (AddHurtRanged k) = [k]
      p _ = []
  in strengthAspectMaybe p

strengthAddArmorMelee :: ItemFull -> Maybe Int
strengthAddArmorMelee =
  let p (AddArmorMelee k) = [k]
      p _ = []
  in strengthAspectMaybe p

strengthAddArmorRanged :: ItemFull -> Maybe Int
strengthAddArmorRanged =
  let p (AddArmorRanged k) = [k]
      p _ = []
  in strengthAspectMaybe p

strengthAddSight :: ItemFull -> Maybe Int
strengthAddSight =
  let p (AddSight k) = [k]
      p _ = []
  in strengthAspectMaybe p

strengthAddSmell :: ItemFull -> Maybe Int
strengthAddSmell =
  let p (AddSmell k) = [k]
      p _ = []
  in strengthAspectMaybe p

strengthAddLight :: ItemFull -> Maybe Int
strengthAddLight =
  let p (AddLight k) = [k]
      p _ = []
  in strengthAspectMaybe p

strengthEqpSlot :: Item -> Maybe (EqpSlot, Text)
strengthEqpSlot item =
  let p (EqpSlot eqpSlot t) = [(eqpSlot, t)]
      p _ = []
  in case strengthFeature p item of
    [] -> Nothing
    [x] -> Just x
    xs -> assert `failure` (xs, item)

strengthToThrow :: Item -> ThrowMod
strengthToThrow item =
  let p (ToThrow tmod) = [tmod]
      p _ = []
  in case strengthFeature p item of
    [] -> ThrowMod 100 100
    [x] -> x
    xs -> assert `failure` (xs, item)

computeTrajectory :: Int -> Int -> Int -> [Point] -> ([Vector], (Speed, Int))
computeTrajectory weight throwVelocity throwLinger path =
  let speed = speedFromWeight weight throwVelocity
      trange = rangeFromSpeedAndLinger speed throwLinger
      btrajectory = take trange $ pathToTrajectory path
  in (btrajectory, (speed, trange))

itemTrajectory :: Item -> [Point] -> ([Vector], (Speed, Int))
itemTrajectory item path =
  let ThrowMod{..} = strengthToThrow item
  in computeTrajectory (jweight item) throwVelocity throwLinger path

totalRange :: Item -> Int
totalRange item = snd $ snd $ itemTrajectory item []

-- TODO: when all below are aspects, define with
-- (EqpSlotAddMaxHP, AddMaxHP k) -> [k]
strengthFromEqpSlot :: EqpSlot -> ItemFull -> Maybe Int
strengthFromEqpSlot eqpSlot =
  case eqpSlot of
    EqpSlotPeriodic -> strengthPeriodic  -- a very crude approximation
    EqpSlotAddMaxHP -> strengthAddMaxHP
    EqpSlotAddMaxCalm -> strengthAddMaxCalm
    EqpSlotAddSpeed -> strengthAddSpeed
    EqpSlotAddSkills -> \itemFull -> sum . EM.elems <$> strengthAddSkills itemFull
    EqpSlotAddHurtMelee -> strengthAddHurtMelee
    EqpSlotAddHurtRanged -> strengthAddHurtRanged
    EqpSlotAddArmorMelee -> strengthAddArmorMelee
    EqpSlotAddArmorRanged -> strengthAddArmorRanged
    EqpSlotAddSight -> strengthAddSight
    EqpSlotAddSmell -> strengthAddSmell
    EqpSlotAddLight -> strengthAddLight
    EqpSlotWeapon -> strengthMelee

strongestSlotNoFilter :: EqpSlot -> [(ItemId, ItemFull)]
                      -> [(Int, (ItemId, ItemFull))]
strongestSlotNoFilter eqpSlot is =
  let f = strengthFromEqpSlot eqpSlot
      g (iid, itemFull) = (\v -> (v, (iid, itemFull))) <$> (f itemFull)
  in sortBy (flip $ Ord.comparing fst) $ mapMaybe g is

strongestSlot :: EqpSlot -> [(ItemId, ItemFull)]
              -> [(Int, (ItemId, ItemFull))]
strongestSlot eqpSlot is =
  let f (_, itemFull) = case strengthEqpSlot $ itemBase itemFull of
        Just (eqpSlot2, _) | eqpSlot2 == eqpSlot -> True
        _ -> False
      slotIs = filter f is
  in strongestSlotNoFilter eqpSlot slotIs

sumSlotNoFilter :: EqpSlot -> [ItemFull] -> Int
sumSlotNoFilter eqpSlot is = assert (eqpSlot /= EqpSlotWeapon) $  -- no 999
  let f = strengthFromEqpSlot eqpSlot
      g itemFull = (* itemK itemFull) <$> f itemFull
  in sum $ mapMaybe g is

sumSkills :: [ItemFull] -> Ability.Skills
sumSkills is =
  let g itemFull = (Ability.scaleSkills (itemK itemFull))
                   <$> strengthAddSkills itemFull
  in foldr Ability.addSkills Ability.zeroSkills $ mapMaybe g is

unknownPrecious :: ItemFull -> Bool
unknownPrecious itemFull =
  Durable `notElem` jfeature (itemBase itemFull)  -- if durable, no risk
  && case itemDisco itemFull of
    Just ItemDisco{itemAE=Just _} -> False
    _ -> Precious `elem` jfeature (itemBase itemFull)

permittedRanged :: ItemFull -> Maybe Int -> Bool
permittedRanged itemFull _ =
  let hasEffects = case itemDisco itemFull of
        Just ItemDisco{itemAE=Just ItemAspectEffect{jeffects=[]}} -> False
        Just ItemDisco{itemAE=Nothing, itemKind=ItemKind{ieffects=[]}} -> False
        _ -> True
  in hasEffects
     && not (unknownPrecious itemFull)
     && case strengthEqpSlot (itemBase itemFull) of
          Just (EqpSlotAddLight, _) -> True
          Just _ -> False
          Nothing -> True

unknownAspect :: (Aspect Dice.Dice -> [Dice.Dice]) -> ItemFull -> Bool
unknownAspect f itemFull =
  case itemDisco itemFull of
    Just ItemDisco{itemAE=Nothing, itemKind=ItemKind{iaspects}} ->
      let unknown x = Dice.minDice x /= Dice.maxDice x
      in or $ concatMap (map unknown . f) iaspects
    _ -> False

unknownMelee :: [ItemFull] -> Bool
unknownMelee =
  let p (AddHurtMelee k) = [k]
      p _ = []
      f itemFull b = b || unknownAspect p itemFull
  in foldr f False