-- | AI strategy operations implemented with the 'Action' monad.
module Game.LambdaHack.StrategyAction
  ( strategy, wait
  ) where

import qualified Data.List as L
import qualified Data.IntMap as IM
import Data.Maybe
import Control.Monad
import Control.Arrow

import Game.LambdaHack.Point
import Game.LambdaHack.Vector
import Game.LambdaHack.Level
import Game.LambdaHack.Actor
import Game.LambdaHack.ActorState
import Game.LambdaHack.Content.ActorKind
import Game.LambdaHack.Utils.Frequency
import Game.LambdaHack.Perception
import Game.LambdaHack.Strategy
import Game.LambdaHack.State
import Game.LambdaHack.Action
import Game.LambdaHack.Actions
import Game.LambdaHack.ItemAction
import Game.LambdaHack.Content.ItemKind
import Game.LambdaHack.Item
import qualified Game.LambdaHack.Effect as Effect
import qualified Game.LambdaHack.Tile as Tile
import qualified Game.LambdaHack.Kind as Kind
import qualified Game.LambdaHack.Feature as F

{-
Monster movement
----------------

Not all monsters use the same algorithm to find the hero.
Some implemented and unimplemented methods are listed below:

* Random
The simplest way to have a monster move is at random.

* Sight
If a monster can see the hero (as an approximation,
we assume it is the case when the hero can see the monster,
unless either of the locations is dark),
the monster should move toward the hero.

* Smell
The hero leaves a trail when moving toward the dungeon.
For a certain timespan (100--200 moves), it is possible
for certain monsters to detect that a hero has been at a certain field.
Once a monster is following a trail, it should move to the
neighboring field where the hero has most recently visited.

* Noise
The hero makes noise. If the distance between the hero
and the monster is small enough, the monster can hear the hero
and moves into the approximate direction of the hero.
-}

-- TODO: improve, split up, etc.
-- | Monster AI strategy based on monster sight, smell, intelligence, etc.
strategy :: Kind.COps -> ActorId -> State -> Perception -> Strategy (Action ())
strategy cops actor oldState@State{splayer = pl, stime = time} per =
  strat
 where
  Kind.COps{ cotile
           , coactor=Kind.Ops{okind}
           , coitem=coitem@Kind.Ops{okind=iokind}
           } = cops
  lvl@Level{lsmell = nsmap, lxsize, lysize} = slevel oldState
  Actor { bkind = ak, bloc = me, bdir = ad, btarget = tgt } =
    getActor actor oldState
  items = getActorItem actor oldState
  mk = okind ak
  delState = deleteActor actor oldState
  enemyVisible a l =
    asight mk && monsterSeesHero cotile per lvl actor a me l ||
    -- Any enemy can be flet if adjacent (e. g., a monster player).
    -- TODO: can this be replaced by setting 'lights' to [me]?
    adjacent lxsize me l
  -- If no heroes on the level, monsters go at each other. TODO: let them
  -- earn XP by killing each other to make this dangerous to the player.
  -- TODO: with some commands blocked, this can't happen now. Find a way
  -- to test it, nevertheless. Have a scroll of monster fury?
  hs = L.map (AHero *** bloc) $
         IM.assocs $ lheroes $ slevel delState
  ms = L.map (AMonster *** bloc) $
         IM.assocs $ lmonsters $ slevel delState
  -- Below, "foe" is the hero (or a monster, or loc) chased by the actor.
  (newTgt, floc, foeVisible) =
    case tgt of
      TEnemy a ll | focusedMonster ->
        if memActor a delState
        then let l = bloc $ getActor a delState
             in if enemyVisible a l
                then (TEnemy a l, Just l, True)
                else if isJust (case closest of (_, m, _) -> m) || me == ll
                     then closest                -- prefer visible foes
                     else (tgt, Just ll, False)  -- last known loc of enemy
        else closest  -- enemy not on the level, temporarily chase others
      TLoc loc -> if me == loc
                  then closest
                  else (tgt, Just loc, False)  -- ignore all and go to loc
      _  -> closest
  closest =
    let hsAndTraitor = if isAMonster pl && memActor pl delState
                       then (pl, bloc $ getPlayerBody delState) : hs
                       else hs
        foes = if L.null hsAndTraitor then ms else hsAndTraitor
        visible = L.filter (uncurry enemyVisible) foes
        foeDist = L.map (\ (a, l) -> (chessDist lxsize me l, l, a)) visible
    in case foeDist of
         [] -> (TCursor, Nothing, False)
         _  -> let (_, l, a) = L.minimum foeDist
               in (TEnemy a l, Just l, True)
  onlyFoe        = onlyMoves (maybe (const False) (==) floc) me
  towardsFoe     = case floc of
                     Nothing -> const mzero
                     Just loc ->
                       let foeDir = towards lxsize me loc
                       in only (\ x -> euclidDistSq lxsize foeDir x <= 1)
  lootHere x     = not $ L.null $ lvl `atI` x
  onlyLoot       = onlyMoves lootHere me
  interestHere x = let t = lvl `at` x
                       ts = map (lvl `at`) $ vicinity lxsize lysize x
                   in Tile.hasFeature cotile F.Exit t ||
                      -- Lit indirectly. E.g., a room entrance.
                      (not (Tile.hasFeature cotile F.Lit t) &&
                       L.any (Tile.hasFeature cotile F.Lit) ts)
  onlyInterest   = onlyMoves interestHere me
  onlyKeepsDir k =
    only (\ x -> maybe True (\ (d, _) -> euclidDistSq lxsize d x <= k) ad)
  onlyKeepsDir_9 = only (\ x -> maybe True (\ (d, _) -> neg x /= d) ad)
  onlyNoMs       = onlyMoves (unoccupied (levelMonsterList delState)) me
  -- Monsters don't see doors more secret than that. Enforced when actually
  -- opening doors, too, so that monsters don't cheat. TODO: remove the code
  -- duplication, though.
  openPower      = Tile.SecretStrength $
                   case strongestSearch coitem items of
                     Just i  -> aiq mk + jpower i
                     Nothing -> aiq mk
  openableHere   = openable cotile lvl openPower
  onlyOpenable   = onlyMoves openableHere me
  accessibleHere = accessible cops lvl me
  onlySensible   = onlyMoves (\ l -> accessibleHere l || openableHere l) me
  focusedMonster = aspeed mk >= 10
  movesNotBack   = maybe id (\ (d, _) -> L.filter (/= neg d)) ad $ moves lxsize
  smells         =
    L.map fst $
    L.sortBy (\ (_, s1) (_, s2) -> compare s2 s1) $
    L.filter (\ (_, s) -> s > 0) $
    L.map (\ x -> let sm = Tile.smelltime $ IM.findWithDefault
                             (Tile.SmellTime 0) (me `shift` x) nsmap
                  in (x, (sm - time) `max` 0)) movesNotBack
  attackDir d = dirToAction actor newTgt True  `liftM` d
  moveDir d   = dirToAction actor newTgt False `liftM` d

  strat =
    foeVisible .=> attackDir (onlyFoe moveFreely)
    .| foeVisible .=> liftFrequency (msum seenFreqs)
    .| lootHere me .=> actionPickup
    .| moveDir moveTowards  -- go to last known foe location
    .| attackDir moveAround
  actionPickup = return $ actorPickupItem actor
  tis = lvl `atI` me
  seenFreqs = [applyFreq items 1, applyFreq tis 2,
               throwFreq items 3, throwFreq tis 6] ++ towardsFreq
  applyFreq is multi = toFreq
    [ (benefit * multi,
       applyGroupItem actor (iverbApply ik) i)
    | i <- is,
      let ik = iokind (jkind i),
      let benefit = (1 + jpower i) * Effect.effectToBenefit (ieffect ik),
      benefit > 0,
      asight mk || isymbol ik == '!']
  throwFreq is multi = if adjacent lxsize me (fromJust floc) || not (asight mk)
                       then mzero
                       else toFreq
    [ (benefit * multi,
       projectGroupItem actor (fromJust floc) (iverbProject ik) i)
    | i <- is,
      let ik = iokind (jkind i),
      let benefit =
            - (1 + jpower i) * Effect.effectToBenefit (ieffect ik),
      benefit > 0,
      -- Wasting swords would be too cruel to the player.
      isymbol ik /= ')']
  towardsFreq = map (scaleFreq 30) $ runStrategy $ moveDir moveTowards
  moveTowards = onlySensible $ onlyNoMs (towardsFoe moveFreely)
  moveAround =
    onlySensible $
      (if asight mk then onlyNoMs else id) $
        asmell mk .=> L.foldr ((.|) . return) reject smells
        .| onlyOpenable moveFreely
        .| moveFreely
  moveIQ = aiq mk > 15 .=> onlyKeepsDir 0 moveRandomly
        .| aiq mk > 10 .=> onlyKeepsDir 1 moveRandomly
        .| aiq mk > 5  .=> onlyKeepsDir 2 moveRandomly
        .| onlyKeepsDir_9 moveRandomly
  interestFreq =  -- don't detour towards an interest if already on one
    if interestHere me
    then []
    else map (scaleFreq 3)
           (runStrategy $ onlyInterest (onlyKeepsDir 2 moveRandomly))
  interestIQFreq = interestFreq ++ runStrategy moveIQ
  moveFreely = onlyLoot moveRandomly
               .| liftFrequency (msum interestIQFreq)
               .| moveRandomly
  onlyMoves :: (Point -> Bool) -> Point -> Strategy Vector -> Strategy Vector
  onlyMoves p l = only (\ x -> p (l `shift` x))
  moveRandomly :: Strategy Vector
  moveRandomly = liftFrequency $ uniformFreq (moves lxsize)

dirToAction :: ActorId -> Target -> Bool -> Vector -> Action ()
dirToAction actor tgt allowAttacks dir = do
  -- set new direction
  updateAnyActor actor $ \ m -> m { bdir = Just (dir, 0), btarget = tgt }
  -- perform action
  tryWith (advanceTime actor) $
    -- if the following action aborts, we just advance the time and continue
    -- TODO: ensure time is taken for other aborted actions in this file
    moveOrAttack allowAttacks actor dir

-- | A strategy to always just wait.
wait :: ActorId -> Strategy (Action ())
wait actor = return $ advanceTime actor