local utils = require("nui.utils")
local layout_utils = require("nui.layout.utils")

local u = {
  is_type = utils.is_type,
  calculate_window_size = layout_utils.calculate_window_size,
}

local mod = {}

local function get_child_position(box, child, current_position, canvas_position)
  local position = box.dir == "row" and {
    row = canvas_position.row,
    col = current_position.col,
  } or {
    col = canvas_position.col,
    row = current_position.row,
  }

  if child.component then
    local border = child.component.border
    if border and border._.type == "complex" then
      position.col = position.col + math.floor(border._.size_delta.width / 2 + 0.5)
      position.row = position.row + math.floor(border._.size_delta.height / 2 + 0.5)
    end
  end

  return position
end

---@param parent table Layout.Box
---@param child table Layout.Box
---@param container_size table
---@param growable_dimension_per_factor? number
local function get_child_size(parent, child, container_size, growable_dimension_per_factor)
  local child_size = {
    width = child.size.width,
    height = child.size.height,
  }

  if child.grow and growable_dimension_per_factor then
    if parent.dir == "col" then
      child_size.height = math.floor(growable_dimension_per_factor * child.grow)
    else
      child_size.width = math.floor(growable_dimension_per_factor * child.grow)
    end
  end

  local outer_size = u.calculate_window_size(child_size, container_size)

  local inner_size = {
    width = outer_size.width,
    height = outer_size.height,
  }

  if child.component then
    if child.component.border then
      inner_size.width = inner_size.width - child.component.border._.size_delta.width
      inner_size.height = inner_size.height - child.component.border._.size_delta.height

      if inner_size.height <= 0 then
        local height_adjustment = math.abs(inner_size.height) + 1
        inner_size.height = inner_size.height + height_adjustment
        outer_size.height = outer_size.height + height_adjustment
      end
    end
  end

  return outer_size, inner_size
end

function mod.process(box, meta)
  -- luacov: disable
  if box.mount or box.component or not box.box then
    return error("invalid parameter: box")
  end
  -- luacov: enable

  local container_size = meta.container_size

  -- luacov: disable
  if not u.is_type("number", container_size.width) or not u.is_type("number", container_size.height) then
    return error("invalid value: box.size")
  end
  -- luacov: enable

  local current_position = box.dir == "row" and {
    col = meta.position.col,
    row = 0,
  } or {
    col = 0,
    row = meta.position.row,
  }

  local growable_child_factor = 0

  for _, child in ipairs(box.box) do
    if meta.process_growable_child or not child.grow then
      local position = get_child_position(box, child, current_position, meta.position)
      local outer_size, inner_size = get_child_size(box, child, container_size, meta.growable_dimension_per_factor)

      if child.component then
        child.component:set_layout({
          size = inner_size,
          relative = {
            type = "win",
            winid = meta.winid,
          },
          position = position,
        })
      else
        mod.process(child, {
          winid = meta.winid,
          container_size = outer_size,
          position = position,
        })
      end

      current_position.col = current_position.col + outer_size.width
      current_position.row = current_position.row + outer_size.height
    end

    if child.grow then
      growable_child_factor = growable_child_factor + child.grow
    end
  end

  if meta.process_growable_child or growable_child_factor == 0 then
    return
  end

  local growable_width = container_size.width - current_position.col
  local growable_height = container_size.height - current_position.row
  local growable_dimension = box.dir == "col" and growable_height or growable_width
  local growable_dimension_per_factor = growable_dimension / growable_child_factor

  mod.process(box, {
    winid = meta.winid,
    container_size = meta.container_size,
    position = meta.position,
    process_growable_child = true,
    growable_dimension_per_factor = growable_dimension_per_factor,
  })
end

function mod.mount_box(box)
  for _, child in ipairs(box.box) do
    if child.component then
      child.component:mount()
    else
      mod.mount_box(child)
    end
  end
end

---@param box table Layout.Box
function mod.show_box(box)
  for _, child in ipairs(box.box) do
    if child.component then
      child.component:show()
    else
      mod.show_box(child)
    end
  end
end

function mod.unmount_box(box)
  for _, child in ipairs(box.box) do
    if child.component then
      child.component:unmount()
    else
      mod.unmount_box(child)
    end
  end
end

---@param box table Layout.Box
function mod.hide_box(box)
  for _, child in ipairs(box.box) do
    if child.component then
      child.component:hide()
    else
      mod.hide_box(child)
    end
  end
end

---@param box table Layout.Box
---@return table<string, table>
local function collect_box_components(box, components)
  if not components then
    components = {}
  end

  for _, child in ipairs(box.box) do
    if child.component then
      components[child.component._.id] = child.component
    else
      collect_box_components(child, components)
    end
  end

  return components
end

---@param curr_box table Layout.Box
---@param prev_box table Layout.Box
function mod.process_box_change(curr_box, prev_box)
  if curr_box == prev_box then
    return
  end

  local curr_components = collect_box_components(curr_box)
  local prev_components = collect_box_components(prev_box)

  for id, component in pairs(curr_components) do
    if not prev_components[id] then
      if not component.winid then
        if component._.mounted then
          component:show()
        else
          component:mount()
        end
      end
    end
  end

  for id, component in pairs(prev_components) do
    if not curr_components[id] then
      if component._.mounted then
        if component.winid then
          component:hide()
        end
      end
    end
  end
end

return mod
