{-# LANGUAGE CPP #-}
-- | The unpopulated dungeon generation routine.
module Game.LambdaHack.Server.DungeonGen
  ( FreshDungeon(..), dungeonGen
#ifdef EXPOSE_INTERNAL
    -- * Internal operations
  , convertTileMaps, placeStairs, buildLevel, levelFromCaveKind, findGenerator
#endif
  ) where

import Control.Applicative
import Control.Exception.Assert.Sugar
import Control.Monad
import qualified Data.EnumMap.Strict as EM
import qualified Data.IntMap.Strict as IM
import Data.List
import Data.Maybe

import qualified Game.LambdaHack.Common.Kind as Kind
import Game.LambdaHack.Common.Level
import Game.LambdaHack.Common.Misc
import Game.LambdaHack.Common.Point
import qualified Game.LambdaHack.Common.PointArray as PointArray
import Game.LambdaHack.Common.Random
import qualified Game.LambdaHack.Common.Tile as Tile
import Game.LambdaHack.Common.Time
import Game.LambdaHack.Content.CaveKind
import Game.LambdaHack.Content.ItemKind (ItemKind)
import qualified Game.LambdaHack.Content.ItemKind as IK
import Game.LambdaHack.Content.ModeKind
import Game.LambdaHack.Content.TileKind (TileKind)
import qualified Game.LambdaHack.Content.TileKind as TK
import Game.LambdaHack.Server.DungeonGen.Area
import Game.LambdaHack.Server.DungeonGen.Cave
import Game.LambdaHack.Server.DungeonGen.Place

convertTileMaps :: Kind.COps
                -> Rnd (Kind.Id TileKind) -> Maybe (Rnd (Kind.Id TileKind))
                -> Int -> Int -> TileMapEM
                -> Rnd TileMap
convertTileMaps Kind.COps{cotile}
                cdefTile mcdefTileWalkable cxsize cysize ltile = do
  let f :: Point -> Rnd (Kind.Id TileKind)
      f p = case EM.lookup p ltile of
        Just t -> return t
        Nothing -> cdefTile
  converted1 <- PointArray.generateMA cxsize cysize f
  case mcdefTileWalkable of
    Nothing -> return converted1  -- no walkable tiles for filling the map
    Just cdefTileWalkable -> do  -- some tiles walkable, so ensure connectivity
      -- TODO: perhaps checking connectivity with BFS would be better,
      -- but it's still artibrary how we recover connectivity and we still
      -- need ltile not to break rooms (unless that's a good idea,
      -- but surely it's not for starship hull walls, vaults, fire pits, etc.,
      -- so perhaps all but impenetrable walls is game).
      let passes p@Point{..} array =
            px >= 0 && px <= cxsize - 1
            && py >= 0 && py <= cysize - 1
            && Tile.isWalkable cotile (array PointArray.! p)
          -- If no point blocks on both ends, then I can eventually go
          -- from bottom to top of the map and from left to right
          -- unless there are disconnected areas inside rooms).
          blocksHorizontal (Point x y) array =
            not (passes (Point (x + 1) y) array
                 || passes (Point (x - 1) y) array)
          blocksVertical (Point x y) array =
            not (passes (Point x (y + 1)) array
                 || passes (Point x (y - 1)) array)
          xeven Point{..} = px `mod` 2 == 0
          yeven Point{..} = py `mod` 2 == 0
          connect included blocks walkableTile array =
            let g n c = if included n
                           && not (Tile.isWalkable cotile c)
                           && n `EM.notMember` ltile
                           && blocks n array
                        then walkableTile
                        else c
            in PointArray.imapA g array
      walkable2 <- cdefTileWalkable
      let converted2 = connect xeven blocksHorizontal walkable2 converted1
      walkable3 <- cdefTileWalkable
      let converted3 = connect yeven blocksVertical walkable3 converted2
      walkable4 <- cdefTileWalkable
      let converted4 =
            connect (not . xeven) blocksHorizontal walkable4 converted3
      walkable5 <- cdefTileWalkable
      let converted5 =
            connect (not . yeven) blocksVertical walkable5 converted4
      return converted5

placeStairs :: Kind.COps -> TileMap -> CaveKind -> [Point]
            -> Rnd Point
placeStairs Kind.COps{cotile} cmap CaveKind{..} ps = do
  let dist cmin l _ = all (\pos -> chessDist l pos > cmin) ps
  findPosTry 1000 cmap
    (\p t -> Tile.isWalkable cotile t
             && not (Tile.hasFeature cotile TK.NoActor t)
             && dist 0 p t)  -- can't overwrite stairs with other stairs
    [ dist cminStairDist
    , dist $ cminStairDist `div` 2
    , dist $ cminStairDist `div` 4
    , const $ Tile.hasFeature cotile TK.OftenActor
    , dist $ cminStairDist `div` 8
    ]

-- | Create a level from a cave.
buildLevel :: Kind.COps -> Cave
           -> AbsDepth -> LevelId -> LevelId -> LevelId -> AbsDepth
           -> Int -> Maybe Bool
           -> Rnd Level
buildLevel cops@Kind.COps{ cotile=Kind.Ops{opick, okind}
                         , cocave=Kind.Ops{okind=cokind} }
           Cave{..} ldepth ln minD maxD totalDepth nstairUp escapeFeature = do
  let kc@CaveKind{..} = cokind dkind
      fitArea pos = inside pos . fromArea . qarea
      findLegend pos = maybe clegendLitTile qlegend
                       $ find (fitArea pos) dplaces
      hasEscape p = Tile.kindHasFeature (TK.Cause $ IK.Escape p)
      ascendable  = Tile.kindHasFeature $ TK.Cause (IK.Ascend 1)
      descendable = Tile.kindHasFeature $ TK.Cause (IK.Ascend (-1))
      nightCond kt = not (Tile.kindHasFeature TK.Clear kt)
                     || (if dnight then id else not)
                           (Tile.kindHasFeature TK.Dark kt)
      dcond kt = (cpassable
                  || not (Tile.kindHasFeature TK.Walkable kt))
                 && nightCond kt
      pickDefTile = (fromMaybe $ assert `failure` cdefTile)
                    <$> opick cdefTile dcond
      wcond kt = Tile.kindHasFeature TK.Walkable kt
                 && nightCond kt
      mpickWalkable =
        if cpassable
        then Just $ (fromMaybe $ assert `failure` cdefTile)
                    <$> opick cdefTile wcond
        else Nothing
  cmap <- convertTileMaps cops pickDefTile mpickWalkable cxsize cysize dmap
  -- We keep two-way stairs separately, in the last component.
  let makeStairs :: Bool -> Bool -> Bool
                 -> ( [(Point, Kind.Id TileKind)]
                    , [(Point, Kind.Id TileKind)]
                    , [(Point, Kind.Id TileKind)] )
                 -> Rnd ( [(Point, Kind.Id TileKind)]
                        , [(Point, Kind.Id TileKind)]
                        , [(Point, Kind.Id TileKind)] )
      makeStairs moveUp noAsc noDesc (up, down, upDown) =
        if (if moveUp then noAsc else noDesc) then
          return (up, down, upDown)
        else do
          let cond tk = (if moveUp then ascendable tk else descendable tk)
                        && (not noAsc || not (ascendable tk))
                        && (not noDesc || not (descendable tk))
              stairsCur = up ++ down ++ upDown
              posCur = nub $ sort $ map fst stairsCur
          spos <- placeStairs cops cmap kc posCur
          let legend = findLegend spos
          stairId <- (fromMaybe $ assert `failure` legend)
                     <$> opick legend cond
          let st = (spos, stairId)
              asc = ascendable $ okind stairId
              desc = descendable $ okind stairId
          return $! case (asc, desc) of
                     (True, False) -> (st : up, down, upDown)
                     (False, True) -> (up, st : down, upDown)
                     (True, True)  -> (up, down, st : upDown)
                     (False, False) -> assert `failure` st
  (stairsUp1, stairsDown1, stairsUpDown1) <-
    makeStairs False (ln == maxD) (ln == minD) ([], [], [])
  let !_A = assert (null stairsUp1) ()
  let nstairUpLeft = nstairUp - length stairsUpDown1
  (stairsUp2, stairsDown2, stairsUpDown2) <-
    foldM (\sts _ -> makeStairs True (ln == maxD) (ln == minD) sts)
          (stairsUp1, stairsDown1, stairsUpDown1)
          [1 .. nstairUpLeft]
  -- If only a single tile of up-and-down stairs, add one more stairs down.
  (stairsUp, stairsDown, stairsUpDown) <-
    if null (stairsUp2 ++ stairsDown2)
    then makeStairs False True (ln == minD)
           (stairsUp2, stairsDown2, stairsUpDown2)
    else return (stairsUp2, stairsDown2, stairsUpDown2)
  let stairsUpAndUpDown = stairsUp ++ stairsUpDown
  let !_A = assert (length stairsUpAndUpDown == nstairUp) ()
  let stairsTotal = stairsUpAndUpDown ++ stairsDown
      posTotal = nub $ sort $ map fst stairsTotal
  epos <- placeStairs cops cmap kc posTotal
  escape <- case escapeFeature of
              Nothing -> return []
              Just True -> do
                let legend = findLegend epos
                upEscape <- fmap (fromMaybe $ assert `failure` legend)
                            $ opick legend $ hasEscape 1
                return [(epos, upEscape)]
              Just False -> do
                let legend = findLegend epos
                downEscape <- fmap (fromMaybe $ assert `failure` legend)
                              $ opick legend $ hasEscape (-1)
                return [(epos, downEscape)]
  let exits = stairsTotal ++ escape
      ltile = cmap PointArray.// exits
      -- We reverse the order in down stairs, to minimize long stair chains.
      lstair = ( map fst $ stairsUp ++ stairsUpDown
               , map fst $ stairsUpDown ++ stairsDown )
  -- traceShow (ln, nstairUp, (stairsUp, stairsDown, stairsUpDown)) skip
  litemNum <- castDice ldepth totalDepth citemNum
  lsecret <- randomR (1, maxBound)  -- 0 means unknown
  return $! levelFromCaveKind cops kc ldepth ltile lstair
                              cactorCoeff cactorFreq litemNum citemFreq
                              lsecret (map fst escape)

-- | Build rudimentary level from a cave kind.
levelFromCaveKind :: Kind.COps
                  -> CaveKind -> AbsDepth -> TileMap -> ([Point], [Point])
                  -> Int -> Freqs ItemKind -> Int -> Freqs ItemKind -> Int -> [Point]
                  -> Level
levelFromCaveKind Kind.COps{cotile}
                  CaveKind{..}
                  ldepth ltile lstair lactorCoeff lactorFreq litemNum litemFreq
                  lsecret lescape =
  let lvl = Level
        { ldepth
        , lprio = EM.empty
        , lfloor = EM.empty
        , lembed = EM.empty  -- is populated inside $MonadServer$
        , ltile
        , lxsize = cxsize
        , lysize = cysize
        , lsmell = EM.empty
        , ldesc = cname
        , lstair
        , lseen = 0
        , lclear = 0  -- calculated below
        , ltime = timeZero
        , lactorCoeff
        , lactorFreq
        , litemNum
        , litemFreq
        , lsecret
        , lhidden = chidden
        , lescape
        }
      f n t | Tile.isExplorable cotile t = n + 1
            | otherwise = n
      lclear = PointArray.foldlA f 0 ltile
  in lvl {lclear}

findGenerator :: Kind.COps -> LevelId -> LevelId -> LevelId -> AbsDepth -> Int
              -> (GroupName CaveKind, Maybe Bool)
              -> Rnd Level
findGenerator cops ln minD maxD totalDepth nstairUp
              (genName, escapeFeature) = do
  let Kind.COps{cocave=Kind.Ops{opick}} = cops
  ci <- (fromMaybe $ assert `failure` genName)
        <$> opick genName (const True)
  -- A simple rule for now: level at level @ln@ has depth (difficulty) @abs ln@.
  let ldepth = AbsDepth $ abs $ fromEnum ln
  cave <- buildCave cops ldepth totalDepth ci
  buildLevel cops cave ldepth ln minD maxD totalDepth nstairUp escapeFeature

-- | Freshly generated and not yet populated dungeon.
data FreshDungeon = FreshDungeon
  { freshDungeon    :: !Dungeon   -- ^ maps for all levels
  , freshTotalDepth :: !AbsDepth  -- ^ absolute dungeon depth
  }

-- | Generate the dungeon for a new game.
dungeonGen :: Kind.COps -> Caves -> Rnd FreshDungeon
dungeonGen cops caves = do
  let (minD, maxD) =
        case (IM.minViewWithKey caves, IM.maxViewWithKey caves) of
          (Just ((s, _), _), Just ((e, _), _)) -> (s, e)
          _ -> assert `failure` "no caves" `twith` caves
      (minId, maxId) = (toEnum minD, toEnum maxD)
      freshTotalDepth = assert (signum minD == signum maxD)
                        $ AbsDepth
                        $ max 10 $ max (abs minD) (abs maxD)
  let gen :: (Int, [(LevelId, Level)]) -> (Int, (GroupName CaveKind, Maybe Bool))
          -> Rnd (Int, [(LevelId, Level)])
      gen (nstairUp, l) (n, caveTB) = do
        let ln = toEnum n
        lvl <- findGenerator cops ln minId maxId freshTotalDepth nstairUp caveTB
        -- nstairUp for the next level is nstairDown for the current level
        let nstairDown = length $ snd $ lstair lvl
        return (nstairDown, (ln, lvl) : l)
  (nstairUpLast, levels) <- foldM gen (0, []) $ reverse $ IM.assocs caves
  let !_A = assert (nstairUpLast == 0) ()
  let freshDungeon = EM.fromList levels
  return $! FreshDungeon{..}