###############################################################################
# layout_grids.des: Layouts based on grid structures generated by omnigrid
###############################################################################

: crawl_require("dlua/util.lua")
: crawl_require("dlua/layout/procedural.lua")
: crawl_require("dlua/layout/zonify.lua")
: crawl_require("dlua/layout/hyper.lua")
: crawl_require("dlua/layout/omnigrid.lua")
: crawl_require("dlua/layout/vector.lua")
: crawl_require("dlua/layout/theme.lua")

{{

  function render_grid(e, name, grid, paint)

    local gxm,gym = dgn.max_bounds()
    local build = {
      name = name,
      build_fixture = {
        { pass = "Primary", type = "build", strategy = hyper.place.strategy_primary, max_rooms = 1, generators = {
          { generator = "code", paint_callback = omnigrid_floor_plan, grid_options = grid, paint_options = paint,
            min_size_x = gxm-22, max_size_x = gxm-2, min_size_y = gym-22, max_size_y = gym-2,
            place_padding = 1, analyse = false }
        } }
      }
    }
    hyper.build_layout(e,build)

  end

  function padded_bounds(minpad,maxpad)
    local gxm,gym = dgn.max_bounds()
    local x1,y1,x2,y2 = 1,1,gxm-2,gym-2
    return { x1 = x1 + crawl.random_range(minpad,maxpad),
             y1 = y1 + crawl.random_range(minpad,maxpad),
             x2 = x2 - crawl.random_range(minpad,maxpad),
             y2 = y2 - crawl.random_range(minpad,maxpad) }
  end

  -- Generate a floor plan based on omnigrid subdivision
  function omnigrid_floor_plan(room,options,gen)

    local grid_options = (type(gen.grid_options) == "function") and gen.grid_options() or gen.grid_options
    local paint_options = gen.paint_options or {}
    paint_options.size = room.size

    local grid = omnigrid.subdivide(0,0,room.size.x-1,room.size.y-1,grid_options)

    local paint = {
      { type = "floor", corner1 = { x = 0, y = 0 }, corner2 = { x = room.size.x-1, y = room.size.y-1 }, usage = gen.usage_params }
    }

    return omnigrid.paint(grid, gen.paint_options, paint)

  end

  -- Draws a floor plan based on omnigrid
  function hyper.floors.floor_plan_omnigrid(room,options,gen)

    local paint = omnigrid.omnigrid_paint{
      guaranteed_divides = 0,
      subdivide_initial_chance = 80,  -- 20% chance of a donut
      fill_chance = 100,
      corridor_width = 8,
      minimum_size = 12,
      jitter = true,
      jitter_min = -2,
      jitter_max = 2,
      outer_corridor = true,
      size = room.size,
      paint_func = crawl.one_chance_in(3) and hyper.caves.cave_paint or nil,
      floor_func = crawl.one_chance_in(2) and hyper.caves.cave_paint or nil
    }
    return paint
  end

  function hyper.floors.floor_plan_finegrid(room,options,gen)
    local corridor_width = crawl.random_range(1,3)
    local jitters = {
      { weight = 20, jitter = false },
      { weight = 10, jitter = true, jitter_min = -3, jitter_max = 0 },
      -- With high jitter, it turns into really chaotic random architecture
      { weight = 5, jitter = true, jitter_min = -2, jitter_max = 1 },
      { weight = 5, jitter = true, jitter_min = -2, jitter_max = 2 }
    }
    local chosen = util.random_weighted_from("weight",jitters)

    local paint = omnigrid.omnigrid_paint{
      guaranteed_divides = 6,
      subdivide_initial_chance = 100,  -- 20% chance of a donut
      subdivide_level_multiplier = 0.98,
      corridor_width = corridor_width,
      fill_chance = 80,
      minimum_size = 6,
      jitter = chosen.jitter,
      jitter_min = chosen.jitter_min,
      jitter_max = chosen.jitter_max,
      size = room.size,
      paint_func = crawl.one_chance_in(3) and hyper.caves.cave_paint or nil,
      floor_func = crawl.one_chance_in(3) and hyper.caves.cave_paint or nil
    }
    return paint
  end

  function hyper.floors.floor_plan_microgrid(room,options,gen)
    local paint = omnigrid.omnigrid_paint{
      guaranteed_divides = 5,
      subdivide_initial_chance = 100,  -- 20% chance of a donut
      subdivide_level_multiplier = util.random_range_real(0.9,0.999),
      corridor_width = 1,
      fill_chance = crawl.random_range(50,90),
      minimum_size = 2,
      jitter = false,
      size = room.size,
      paint_func = crawl.one_chance_in(5) and hyper.caves.cave_paint or nil,
      floor_func = crawl.one_chance_in(4) and hyper.caves.cave_paint or nil
    }
    return paint
  end

}}

##############################################################
# layout_big_grid
#
# This layout should appear at most once, so it does not have an
# "allow_dup" tag,
#
# This layout is included provisionally.  It might be better
# to remove it entirely.
#
NAME:   layout_big_grid
DEPTH:  Lair
WEIGHT: 2
ORIENT: encompass
TAGS:   overwritable layout allow_dup unrand no_rotate no_pool_fixup
TAGS:   no_hmirror no_vmirror layout_type_open
{{
  if is_validating() then return; end
  local edge = crawl.x_chance_in_y(2,3)
  render_grid(_G, "Big Grid",
    {
      guaranteed_divides = edge and 0 or 2,
      -- 20% chance of a donut *if* we have an outer corridor
      subdivide_initial_chance = edge and 80 or 100,
      subdivide_level_multiplier = edge and 0.80 or 0.90,
      minimum_size = 16,
    },
    {
      fill_chance = 100,
      corridor_width = crawl.random_range(6,8),
      outer_corridor = edge,
      jitter = edge and crawl.coinflip(),
      jitter_min = -2,
      jitter_max = 2,
    } )

  zonify.grid_fill_zones(1, "rock_wall")
}}

##############################################################
# layout_small_grid
#
NAME:   layout_small_grid
DEPTH:  D:9-, Lair, Depths, Pan
WEIGHT: 5 (D), 5 (Lair), 3 (Depths), 10 (Pan)
ORIENT: encompass
TAGS:   overwritable layout allow_dup unrand no_rotate no_pool_fixup
TAGS:   no_hmirror no_vmirror layout_type_passages
{{
  if is_validating() then return; end

  local edge = crawl.one_chance_in(4)

  render_grid(_G, "Small Grid",
    {
      guaranteed_divides = 4,
      subdivide_initial_chance = 120,
      subdivide_level_multiplier = .95,
      minimum_size = 6,
    },
    {
      fill_chance = 80,
      corridor_width = crawl.random_range(1,2),
      outer_corridor = edge,
      jitter = crawl.coinflip(),
      jitter_min = -1,
      jitter_max = crawl.random_range(0,1),
    } )
}}

##############################################################
# layout_long_grid
#
NAME:   layout_long_grid
DEPTH:  D:9-, Lair, Depths, Pan
WEIGHT: 10 (D), 5 (Lair), 7 (Depths), 10 (Pan)
ORIENT: encompass
TAGS:   overwritable layout allow_dup unrand no_rotate no_pool_fixup
TAGS:   no_hmirror no_vmirror layout_type_passages
{{
  if is_validating() then return; end
  local min_size = you.in_branch("lair") and 1 or 2
  local max_size = you.in_branch("lair") and 4 or 3
  local size = crawl.random_range(min_size,max_size)

  local grid_opts = {
    -- Keep subdividing until the areas are too small
    subdivide_initial_chance = 100,
    subdivide_level_multiplier = 1,
    minimum_size_x = size * 2,
    minimum_size_y = size * crawl.random_range(4,6),
  }

  local min_fill = 40 + size * 10
  local max_fill = 80 + size * 5

  local paint_opts = {
    fill_chance = crawl.random_range(min_fill,max_fill),
    corridor_width = size,
    outer_corridor = you.in_branch("lair"),
    jitter = false,
  }

  -- In D, for fairness, allow some narrow corridors (sometimes in Lair)
  if you.in_branch("D")
    or (you.in_branch("lair") and size>1 and crawl.one_chance_in(3)) then
    paint_opts.corridor_width = 1
    paint_opts.jitter = true
    paint_opts.jitter_min = -1
    paint_opts.jitter_max = 0
    if you.in_branch("D") then paint_opts.outer_corridor = crawl.coinflip() end
  end

  -- Choose an axis and switch sizes
  local which = "x"
  if crawl.coinflip() then
    which = "y"
    grid_opts.minimum_size_x,grid_opts.minimum_size_y = grid_opts.minimum_size_y,grid_opts.minimum_size_x
  end
  -- The effect this callback has is to keep subdividing a given axis
  -- until we reach the minimum size - *then* start dividing the other axis
  grid_opts.choose_axis = function(width,height,depth)
    return which
  end
  render_grid(_G, "Long Grid", grid_opts, paint_opts)
}}

##############################################################
# layout_layered_grid
#
# Generates up to 3 grids of different granularities, then layers
# then on top of each other. At its simplest reduction this looks like
# forbidden_donut but with a randomly-placed hole. At its most complex
# it does all kinds of interesting things.
#
# Attempting to use this layout causes LUA runtime errors.
#
NAME:   layout_layered_grid
DEPTH:  D, Depths, Crypt, Lair
WEIGHT: 0
ORIENT: encompass
TAGS:   overwritable layout allow_dup unrand no_pool_fixup layout_type_passages
TAGS:   no_rotate no_vmirror no_hmirror
{{
  if is_validating() then return; end

  local weights = {
    { weight = 20, primary = hyper.floors.floor_plan_omnigrid },
    { weight = 30, primary = hyper.floors.floor_plan_finegrid },
    { weight = 50, primary = hyper.floors.floor_plan_microgrid },
    { weight = 10, primary = hyper.floors.floor_plan_omnigrid, secondary = hyper.floors.floor_plan_finegrid },
    { weight = 10, primary = hyper.floors.floor_plan_omnigrid, secondary = hyper.floors.floor_plan_microgrid },
    { weight = 10, primary = hyper.floors.floor_plan_finegrid, secondary = hyper.floors.floor_plan_microgrid },
    -- Very rare because it's somewhat liable to end up as a big mostly empty room
    { weight = 5, primary = hyper.floors.floor_plan_omnigrid, secondary = hyper.floors.floor_plan_finegrid, tertiary = hyper.floors.floor_plan_microgrid }
  }

  local chosen = util.random_weighted_from("weight",weights)

  local gxm,gym = dgn.max_bounds()
  local build = {
    name = "Hyper Grid",
    build_fixture = {
      { pass = "Primary", type = "build", strategy = hyper.place.strategy_primary, max_rooms = 1, generators = {
        { generator = "code", paint_callback = chosen.primary, min_size_x = gxm-22, max_size_x = gxm-2, min_size_y = gym-22, max_size_y = gym-2, place_padding = 1 }
      } },
      { pass = "Secondary", type = "build", strategy = hyper.place.strategy_primary, max_rooms = 1, enabled = chosen.secondary ~= nil, generators = {
        { generator = "code", paint_callback = chosen.secondary, min_size_x = gxm-22, max_size_x = gxm-2, min_size_y = gym-22, max_size_y = gym-2, place_padding = 1 }
      } },
      { pass = "Tertiary", type = "build", strategy = hyper.place.strategy_primary, max_rooms = 1, enabled = chosen.tertiary ~= nil, generators = {
        { generator = "code", paint_callback = chosen.tertiary, min_size_x = gxm-22, max_size_x = gxm-2, min_size_y = gym-22, max_size_y = gym-2, place_padding = 1 }
      } }
    }
  }
  hyper.build_layout(_G,build)
  zonify.grid_fill_zones(1, "rock_wall")
}}

##############################################################
# layout_grid_shapes
#
# Draws a random grid but can use a variety of different shapes to
# draw each grid area.
#
NAME:   layout_grid_shapes
DEPTH:  Snake, Pan
WEIGHT: 5 (Snake), 10 (Pan)
ORIENT: encompass
TAGS:   overwritable layout allow_dup unrand layout_type_open_caves
{{
  if is_validating() then return; end

  -- TODO: This needs enough variation options to be customised more in each
  -- branch as it is. This can be done a few ways: specific shapes for
  -- different branches; changing subdivide options; caves on/off; corridor
  -- widths; distortion functions on the shapes (in snake); different types of
  -- cavey func; etc.
  -- Also once grid connectivity stuff is worked out, would be good to not
  -- draw some of the outer shapes whilst not breaking connectivity.

  local gxm,gym = dgn.max_bounds()
  extend_map { width = gxm, height = gym, fill = 'x' }

  local grid_opts = {
    guaranteed_divides = 2,
    subdivide_initial_chance = 100,
    subdivide_level_multiplier = .9,
    minimum_size = 8,
  }

  local cavey = you.in_branch("snake")
                or (you.in_branch("pan") and crawl.coinflip())
  local distort = you.in_branch("zot")

  local max_corridor = crawl.random_range((cavey or distort) and 4 or 2,10)
  local bounds = padded_bounds(max_corridor,max_corridor + crawl.random_range(1,6))
  local grid = omnigrid.subdivide(bounds.x1,bounds.y1,bounds.x2,bounds.y2,grid_opts)

  -- TODO: Allow specifying different inner and outer shapes. This will make it easy to do a multishape option
  -- but always have a big enough outer shape. Also, more shapes (including rounded corners rect.)
  local shapes = {
    { weight = 10, func = function(w,h)
                            return procedural.scale(procedural.translate(primitive.distance,1),w/2,h/2)
                          end },
    { weight = 10, func = function(w,h)
                            return procedural.scale(procedural.translate(primitive.octagon(.25),1),w/2,h/2)
                          end },
    { weight = 10, func = function(w,h)
                            return procedural.scale(procedural.translate(primitive.diamond(),1),w/2,h/2)
                          end },
    { weight = 10, func = function(w,h)
                            return procedural.scale(procedural.translate(primitive.hexagon(.5),1),w/2,h/2)
                          end },
    { weight = 10, func = function(w,h)
                            return procedural.scale(procedural.translate(primitive.triangle(),1),w/2,h/2)
                          end }
  }

  local shape = util.random_weighted_from("weight",shapes).func
  local fshape = shape
  -- Convert shape to a cavey one
  if cavey then
    local fcave = procedural.worley_diff { scale = .5 }
    fshape = function(w,h)
      local base = shape(w,h)
      return function(x,y,gx,gy)
        return base(x,y) + fcave(gx,gy)/4
      end
    end
  end

  for i,area in ipairs(grid) do
    shape = util.random_weighted_from("weight",shapes).func
    if distort then
      fshape = procedural.transform.damped_distortion(shape)
    end
    -- Work out dimensions and half-corridor size for this block
    area.w,area.h = area.x2 - area.x1 + 1,area.y2 - area.y1 + 1
    area.corridor = math.min(math.floor(math.min(area.w,area.h)/4),math.floor(max_corridor/2))
    -- Draw the bigger container shape
    procedural.render_map_area(_G,area.x1-area.corridor,area.y1-area.corridor,
                               area.x2+area.corridor,area.y2+area.corridor,
                               fshape(area.w+2*area.corridor,area.h+2*area.corridor),'.')
    -- Chance of not filling increases the smaller the areas are
    if crawl.random_real() < (.5 + math.max(area.w,area.h) / (math.max(gxm,gym) * 2)) then
      shape = util.random_weighted_from("weight",shapes).func
      if distort then
        fshape = procedural.transform.damped_distortion(shape)
      end
      -- Draw the central column
      procedural.render_map_area(_G,area.x1+area.corridor,area.y1+area.corridor,
                                 area.x2-area.corridor,area.y2-area.corridor,
                                 fshape(area.w-2*area.corridor,area.h-2*area.corridor),'x')
    end
  end

  zonify.map_fill_zones(_G, 1, 'x')

  if cavey then theme.D.caves(_G)
  else theme.level_material(_G) end
}}

##############################################################
# layout_grid_maze
#
# Builds a complex maze by starting off with a subdivided grid then
# iteratively linking up rooms until all original cells are connected.
# TODO: Fortify edges;
#       randomise door positions;
#       something unique in Pan;
#       maybe an Elf version
#
NAME:   layout_grid_maze
DEPTH:  D:9-, Snake, Tar
WEIGHT: 5 (D), 10 (Snake), 20 (Tar)
ORIENT: encompass
TAGS:   overwritable layout allow_dup unrand layout_type_passages
{{
  if is_validating() then return; end

  local gxm,gym = dgn.max_bounds()
  extend_map { width = gxm, height = gym, fill = 'x' }

  local thin = crawl.one_chance_in(3)       -- Use thin or thick walls?
  local detree = crawl.x_chance_in_y(9,10)  -- Cut out more walls to make a less tree-like layout
  local doors = crawl.x_chance_in_y(2,3)    -- Create doors sometimes instead of just holes in walls
  local fortify = you.in_branch("Tar")      -- Add wall crennelations?

  if you.in_branch("Lair") or you.in_branch("Snake") then
    thin = false
  elseif you.in_branch("D") then
    thin = true
  end

  local grid_opts = {
    guaranteed_divides = 5,
    subdivide_initial_chance = 100,
    subdivide_level_multiplier = util.random_range_real(.97,.99),
    minimum_size = crawl.random_range(thin and 3 or 4,thin and 5 or 6)
  }

  if you.in_branch("Dis") then
    grid_opts.minimum_size = math.max(grid_opts.minimum_size, thin and 4 or 5)
  end

  local bounds = padded_bounds(2,10)
  local grid = omnigrid.subdivide(bounds.x1,bounds.y1,bounds.x2,bounds.y2,grid_opts)

  -- Connect up the cells
  local minbord = thin and 2 or 3
  local fstyle = function() return doors and crawl.one_chance_in(3) and "door" or "open" end
  local groups = omnigrid.connect { grid = grid, min_border_length = minbord, style_func = fstyle }
  if detree then
    groups = omnigrid.connect { grid = grid, groups = groups,
                                max_iterations = crawl.random_range(math.ceil(#grid/20),math.floor(#grid/5)),
                                border_weight = function(b) return b.style == nil and b.len >= minbord and 10 or 0 end,
                                min_groups = -1, style_func = fstyle }
  end

  local function maybefortify(b)
    -- Fortify the wall
    if fortify and b.len>=3 then
      -- TODO: This algorithm (decorating procedurally along a wall) has more potential use
      local horiz = (b.y1==b.y2)
      local norm = vector.normals[b.dir+1]
      -- Choose a random end of the wall to start from
      local from_end = crawl.coinflip()
      -- Offset by the negative normal to move to a grid inside the room.
      local end1 = { x = b.x1 - norm.x, y = b.y1 - norm.y }
      local end2 = { x = b.x2 - norm.x, y = b.y2 - norm.y }
      if from_end then end1,end2 = end2,end1 end
      local bail = false
      local draw = true
      while not bail do
        if horiz then end1.x = end1.x + (from_end and -1 or 1)
        else end1.y = end1.y + (from_end and -1 or 1) end
        if (end1.x==end2.x and end1.y==end2.y) then
          bail = true
        elseif draw and (b.mid == nil or (end1.x ~= b.mid.x and end1.y ~= b.mid.y)) then
          mapgrd[end1.x][end1.y] = "x"
        end
        draw = not draw
      end
    end
  end

  -- Set up the render functions
  local function renderborderthin(cell,b)
    -- Only fill bottom and right borders. These borders won't
    -- exist for cells on the bottom or right of the grid so we
    -- don't need to worry about them.
    if b.dir>1 then
      if b.style ~= "open" then
        if b.dir==2 and b.x1 == cell.x1 then
          fill_area { x1 = b.x1-1, y1 = b.y1, x2 = b.x2, y2 = b.y2, fill = 'x' }
        elseif b.dir==3 and b.y1 == cell.y1 then
          fill_area { x1 = b.x1, y1 = b.y1-1, x2 = b.x2, y2 = b.y2, fill = 'x' }
        else
          fill_area { x1 = b.x1, y1 = b.y1, x2 = b.x2, y2 = b.y2, fill = 'x' }
        end
        -- Maybe paint a door
        b.mid = { x = math.floor((b.x1+b.x2)/2), y = math.floor((b.y1+b.y2)/2) }
        if b.style == "door" then
          mapgrd[b.mid.x][b.mid.y] = '+'
        end
        maybefortify(b)
      end
    elseif b.style ~= "open" then
      -- Fortify non-rendered wall side
      local norm = vector.normals[b.dir+1]
      local b2 = {x1=b.x1+norm.x,x2=b.x2+norm.x,y1=b.y1+norm.y,y2=b.y2+norm.y,dir=b.dir,mid=b.mid,len=b.len}
      b2.mid = { x = math.floor((b2.x1+b2.x2)/2), y = math.floor((b2.y1+b2.y2)/2) }
      maybefortify(b2)
    end
  end

  local function renderborderthick(cell,b)
    if b.style ~= "open" then
      fill_area { x1 = b.x1, y1 = b.y1, x2 = b.x2, y2 = b.y2, fill = 'x' }
      if b.style == "door" then
        -- Choose whether door is placed on this or opposite wall side
        -- A gap will be painted on the side that doesn't get the door
        if b.inverse.door_side == nil then
          b.door_side = crawl.coinflip()
        else
          b.door_side = not b.inverse.door_side
        end
        b.mid = { x = math.floor((b.x1+b.x2)/2), y = math.floor((b.y1+b.y2)/2) }
        mapgrd[b.mid.x][b.mid.y] = b.door_side and '+' or '.'
      end
      maybefortify(b)
    end
  end

  local renderborder = thin and renderborderthin or renderborderthick

  local function rendercell(cell)
    fill_area{x1=cell.x1, y1=cell.y1, x2=cell.x2, y2=cell.y2, fill='.' }
    for i,b in ipairs(cell.borders) do
      renderborder(cell,b)
    end
  end

  for i,area in ipairs(grid) do
    rendercell(area)
  end

  zonify.map_fill_zones(_G, 1, 'x')
  theme.level_material(_G)

}}
MAP
ENDMAP
