-- | Operations on the 'Area' type that involve random numbers.
module Game.LambdaHack.Server.DungeonGen.AreaRnd
  ( -- * Picking points inside areas
    xyInArea, mkRoom, mkVoidRoom
    -- * Choosing connections
  , connectGrid, randomConnection
    -- * Plotting corridors
  , Corridor, connectPlaces
  ) where

import qualified Data.EnumSet as ES
import Data.Maybe

import Game.LambdaHack.Common.PointXY
import Game.LambdaHack.Common.Random
import Game.LambdaHack.Common.VectorXY
import Game.LambdaHack.Server.DungeonGen.Area
import Control.Exception.Assert.Sugar

-- Picking random points inside areas

-- | Pick a random point within an area.
xyInArea :: Area -> Rnd PointXY
xyInArea area = do
  let (x0, y0, x1, y1) = fromArea area
  rx <- randomR (x0, x1)
  ry <- randomR (y0, y1)
  return $ PointXY (rx, ry)

-- | Create a random room according to given parameters.
mkRoom :: (X, Y)    -- ^ minimum size
       -> (X, Y)    -- ^ maximum size
       -> Area      -- ^ the containing area, not the room itself
       -> Rnd Area
mkRoom (xm, ym) (xM, yM) area = do
  let (x0, y0, x1, y1) = fromArea area
  assert (xm <= x1 - x0 + 1 && ym <= y1 - y0 + 1) skip
  let a0 = (x0, y0, x1 - xm + 1, y1 - ym + 1)
      area0 = fromMaybe (assert `failure` a0) $ toArea a0
  PointXY (rx0, ry0) <- xyInArea area0
  let sX = rx0 + xm - 1
      sY = ry0 + ym - 1
      eX = min x1 (rx0 + xM - 1)
      eY = min y1 (ry0 + yM - 1)
      a1 = (sX, sY, eX, eY)
      area1 = fromMaybe (assert `failure` a1) $ toArea a1
  PointXY (rx1, ry1) <- xyInArea area1
  let a3 = (rx0, ry0, rx1, ry1)
      area3 = fromMaybe (assert `failure` a3) $ toArea a3
  return area3

-- | Create a void room, i.e., a single point area within the designated area.
mkVoidRoom :: Area -> Rnd Area
mkVoidRoom area = do
  -- Pass corridors closer to the middle of the grid area, if possible.
  let core = fromMaybe area $ shrink area
  pxy <- xyInArea core
  return $ trivialArea pxy

-- Choosing connections between areas in a grid

-- | Pick a subset of connections between adjacent areas within a grid until
-- there is only one connected component in the graph of all areas.
connectGrid :: (X, Y) -> Rnd [(PointXY, PointXY)]
connectGrid (nx, ny) = do
  let unconnected = ES.fromList [ PointXY (x, y)
                                | x <- [0..nx-1], y <- [0..ny-1] ]
  -- Candidates are neighbours that are still unconnected. We start with
  -- a random choice.
  rx <- randomR (0, nx-1)
  ry <- randomR (0, ny-1)
  let candidates = ES.fromList [ PointXY (rx, ry) ]
  connectGrid' (nx, ny) unconnected candidates []

connectGrid' :: (X, Y) -> ES.EnumSet PointXY -> ES.EnumSet PointXY
             -> [(PointXY, PointXY)]
             -> Rnd [(PointXY, PointXY)]
connectGrid' (nx, ny) unconnected candidates acc
  | ES.null candidates = return $ map sortPointXY acc
  | otherwise = do
      c <- oneOf (ES.toList candidates)
      -- potential new candidates:
      let ns = ES.fromList $ vicinityCardinalXY (0, 0, nx-1, ny-1) c
          nu = ES.delete c unconnected  -- new unconnected
          -- (new candidates, potential connections):
          (nc, ds) = ES.partition (`ES.member` nu) ns
      new <- if ES.null ds
             then return id
             else do
               d <- oneOf (ES.toList ds)
               return ((c, d) :)
      connectGrid' (nx, ny) nu
        (ES.delete c (candidates `ES.union` nc)) (new acc)

-- | Pick a single random connection between adjacent areas within a grid.
randomConnection :: (X, Y) -> Rnd (PointXY, PointXY)
randomConnection (nx, ny) =
  assert (nx > 1 && ny > 0 || nx > 0 && ny > 1 `blame` "wrong connection"
                                               `twith` (nx, ny)) $ do
  rb <- oneOf [False, True]
  if rb || ny <= 1
    then do
      rx  <- randomR (0, nx-2)
      ry  <- randomR (0, ny-1)
      return (PointXY (rx, ry), PointXY (rx+1, ry))
    else do
      rx  <- randomR (0, nx-1)
      ry  <- randomR (0, ny-2)
      return (PointXY (rx, ry), PointXY (rx, ry+1))

-- Plotting individual corridors between two areas

-- | The choice of horizontal and vertical orientation.
data HV = Horiz | Vert

-- | The coordinates of consecutive fields of a corridor.
type Corridor = [PointXY]

-- | Create a corridor, either horizontal or vertical, with
-- a possible intermediate part that is in the opposite direction.
mkCorridor :: HV            -- ^ orientation of the starting section
           -> PointXY       -- ^ starting point
           -> PointXY       -- ^ ending point
           -> Area          -- ^ the area containing the intermediate point
           -> Rnd Corridor  -- ^ straight sections of the corridor
mkCorridor hv (PointXY (x0, y0)) (PointXY (x1, y1)) b = do
  PointXY (rx, ry) <- xyInArea b
  case hv of
    Horiz -> return $ map PointXY [(x0, y0), (rx, y0), (rx, y1), (x1, y1)]
    Vert  -> return $ map PointXY [(x0, y0), (x0, ry), (x1, ry), (x1, y1)]

-- | Try to connect two interiors of places with a corridor.
-- Choose entrances at least 4 or 3 tiles distant from the edges, if the place
-- is big enough. Note that with @pfence == FNone@, the area considered
-- is the strict interior of the place, without the outermost tiles.
connectPlaces :: (Area, Area) -> (Area, Area) -> Rnd Corridor
connectPlaces (sa, so) (ta, to) = do
  let (_, _, sx1, sy1) = fromArea sa
      (_, _, sox1, soy1) = fromArea so
      (tx0, ty0, _, _) = fromArea ta
      (tox0, toy0, _, _) = fromArea to
  assert (sx1 <= tx0  || sy1 <= ty0  `blame` (sa, ta)) skip
  assert (sx1 <= sox1 || sy1 <= soy1 `blame` (sa, so)) skip
  assert (tx0 >= tox0 || ty0 >= toy0 `blame` (ta, to)) skip
  let trim area =
        let (x0, y0, x1, y1) = fromArea area
            trim4 (v0, v1) | v1 - v0 < 6 = (v0, v1)
                           | v1 - v0 < 8 = (v0 + 3, v1 - 3)
                           | otherwise = (v0 + 4, v1 - 4)
            (nx0, nx1) = trim4 (x0, x1)
            (ny0, ny1) = trim4 (y0, y1)
        in fromMaybe (assert `failure` area) $ toArea (nx0, ny0, nx1, ny1)
  PointXY (sx, sy) <- xyInArea $ trim so
  PointXY (tx, ty) <- xyInArea $ trim to
  let hva sarea tarea = do
        let (_, _, zsx1, zsy1) = fromArea sarea
            (ztx0, zty0, _, _) = fromArea tarea
            xa = (zsx1+2, min sy ty, ztx0-2, max sy ty)
            ya = (min sx tx, zsy1+2, max sx tx, zty0-2)
            xya = (zsx1+2, zsy1+2, ztx0-2, zty0-2)
        case toArea xya of
          Just xyarea -> fmap (\hv -> (hv, Just xyarea)) (oneOf [Horiz, Vert])
          Nothing ->
            case toArea xa of
              Just xarea -> return (Horiz, Just xarea)
              Nothing -> return (Vert, toArea ya) -- Vertical bias.
  (hvOuter, areaOuter) <- hva so to
  (hv, area) <- case areaOuter of
    Just arenaOuter -> return (hvOuter, arenaOuter)
    Nothing -> do
      -- TODO: let mkCorridor only pick points on the floor fence
      (hvInner, aInner) <- hva sa ta
      let yell = assert `failure` (sa, so, ta, to, areaOuter, aInner)
          areaInner = fromMaybe yell aInner
      return (hvInner, areaInner)
  -- We cross width one places completely with the corridor, for void
  -- rooms and others (e.g., one-tile wall room then becomes a door, etc.).
  let (p0, p1) = case hv of
        Horiz -> (PointXY (sox1, sy), PointXY (tox0, ty))
        Vert  -> (PointXY (sx, soy1), PointXY (tx, toy0))
  -- The condition imposed on mkCorridor are tricky: there might not always
  -- exist a good intermediate point if the places are allowed to be close
  -- together and then we let the intermediate part degenerate.
  mkCorridor hv p0 p1 area