if ... ~= "__flib__.bounding-box" then
  return require("__flib__.bounding-box")
end

local flib_position = require("__flib__.position")

--- Utilities for manipulating bounding boxes. All functions support both the shorthand and explicit syntaxes for boxes
--- and positions, and will preserve the syntax that was passed in. Boxes are considered immutable; all functions will
--- return new boxes.
--- ```lua
--- local flib_bounding_box = require("__flib__.bounding-box")
--- ```
--- @class flib_bounding_box
local flib_bounding_box = {}

--- Return a new box expanded to the nearest tile edges.
--- @param box BoundingBox
--- @return BoundingBox
function flib_bounding_box.ceil(box)
  if box.left_top then
    return {
      left_top = { x = math.floor(box.left_top.x), y = math.floor(box.left_top.y) },
      right_bottom = { x = math.ceil(box.right_bottom.x), y = math.ceil(box.right_bottom.y) },
    }
  else
    return {
      { math.floor(box[1][1]), math.floor(box[1][2]) },
      { math.ceil(box[2][1]), math.ceil(box[2][2]) },
    }
  end
end

--- Calculate the centerpoint of the box.
--- @param box BoundingBox
--- @return MapPosition
function flib_bounding_box.center(box)
  if box.left_top then
    return {
      x = (box.left_top.x + box.right_bottom.x) / 2,
      y = (box.left_top.y + box.right_bottom.y) / 2,
    }
  else
    return {
      (box[1][1] + box[2][1]) / 2,
      (box[1][2] + box[2][2]) / 2,
    }
  end
end

--- Check if the first box contains the second box.
--- @param box1 BoundingBox
--- @param box2 BoundingBox
--- @return boolean
function flib_bounding_box.contains_box(box1, box2)
  local box1 = flib_bounding_box.ensure_explicit(box1)
  local box2 = flib_bounding_box.ensure_explicit(box2)

  return box1.left_top.x <= box2.left_top.x
    and box1.left_top.y <= box2.left_top.y
    and box1.right_bottom.x >= box2.right_bottom.x
    and box1.right_bottom.y >= box2.right_bottom.y
end

--- Check if the given box contains the given position.
--- @param box BoundingBox
--- @param pos MapPosition
--- @return boolean
function flib_bounding_box.contains_position(box, pos)
  local box = flib_bounding_box.ensure_explicit(box)
  local pos = flib_position.ensure_explicit(pos)
  return box.left_top.x <= pos.x
    and box.left_top.y <= pos.y
    and box.right_bottom.x >= pos.x
    and box.right_bottom.y >= pos.y
end

--- Return the box in explicit form.
--- @param box BoundingBox
--- @return BoundingBox
function flib_bounding_box.ensure_explicit(box)
  return {
    left_top = flib_position.ensure_explicit(box.left_top or box[1]),
    right_bottom = flib_position.ensure_explicit(box.right_bottom or box[2]),
  }
end

--- Return the box in shorthand form.
--- @param box BoundingBox
--- @return BoundingBox
function flib_bounding_box.ensure_short(box)
  return {
    flib_position.ensure_short(box.left_top or box[1]),
    flib_position.ensure_short(box.right_bottom or box[2]),
  }
end

--- Return a new box with initial dimensions box1, expanded to contain box2.
--- @param box1 BoundingBox
--- @param box2 BoundingBox
--- @return BoundingBox
function flib_bounding_box.expand_to_contain_box(box1, box2)
  local box2 = flib_bounding_box.ensure_explicit(box2)

  if box1.left_top then
    return {
      left_top = {
        x = math.min(box1.left_top.x, box2.left_top.x),
        y = math.min(box1.left_top.y, box2.left_top.y),
      },
      right_bottom = {
        x = math.max(box1.right_bottom.x, box2.right_bottom.x),
        y = math.max(box1.right_bottom.y, box2.right_bottom.y),
      },
    }
  else
    return {
      {
        math.min(box1[1][1], box2.left_top.x),
        math.min(box1[1][2], box2.left_top.y),
      },
      {
        math.max(box1[2][1], box2.right_bottom.x),
        math.max(box1[2][2], box2.right_bottom.y),
      },
    }
  end
end

--- Return a new box expanded to contain the given position.
--- @param box BoundingBox
--- @param pos MapPosition
--- @return BoundingBox
function flib_bounding_box.expand_to_contain_position(box, pos)
  pos = flib_position.ensure_explicit(pos)

  if box.left_top then
    return {
      left_top = { x = math.min(box.left_top.x, pos.x), y = math.min(box.left_top.y, pos.y) },
      right_bottom = { x = math.max(box.right_bottom.x, pos.x), y = math.max(box.right_bottom.y, pos.y) },
    }
  else
    return {
      { math.min(box[1][1], pos.x), math.min(box[1][2], pos.y) },
      { math.max(box[2][1], pos.x), math.max(box[2][2], pos.y) },
    }
  end
end

--- Return a new box shrunk to the nearest tile edges.
--- @param box BoundingBox
--- @return BoundingBox
function flib_bounding_box.floor(box)
  if box.left_top then
    return {
      left_top = { x = math.ceil(box.left_top.x), y = math.ceil(box.left_top.y) },
      right_bottom = { x = math.floor(box.right_bottom.x), y = math.floor(box.right_bottom.y) },
    }
  else
    return {
      { math.ceil(box[1][1]), math.ceil(box[1][2]) },
      { math.floor(box[2][1]), math.floor(box[2][2]) },
    }
  end
end

--- Create a new box from a centerpoint and dimensions.
--- @param center MapPosition
--- @param width number
--- @param height number
--- @return BoundingBox
function flib_bounding_box.from_dimensions(center, width, height)
  if center.x then
    return {
      left_top = { x = center.x - width / 2, y = center.y - height / 2 },
      right_bottom = { x = center.x + width / 2, y = center.y + height / 2 },
    }
  else
    return {
      { center[1] - width / 2, center[2] - height / 2 },
      { center[1] + width / 2, center[2] + height / 2 },
    }
  end
end

--- Create a 1x1 box from the given position, optionally snapped to the containing tile edges.
--- @param pos MapPosition
--- @param snap boolean?
--- @return BoundingBox
function flib_bounding_box.from_position(pos, snap)
  if snap then
    pos = flib_position.floor(pos)
  else
    pos = flib_position.sub(pos, { 0.5, 0.5 })
  end
  local x = pos.x or pos[1]
  local y = pos.y or pos[2]
  if pos.x then
    return {
      left_top = { x = x, y = y },
      right_bottom = { x = x + 1, y = y + 1 },
    }
  else
    return {
      { x, y },
      { x + 1, y + 1 },
    }
  end
end

--- Calculate the height of the box.
--- @param box BoundingBox
--- @return number
function flib_bounding_box.height(box)
  if box.left_top then
    return box.right_bottom.y - box.left_top.y
  else
    return box[2][2] - box[1][2]
  end
end

--- Check if the first box intersects (overlaps) the second box.
--- @param box1 BoundingBox
--- @param box2 BoundingBox
--- @return boolean
function flib_bounding_box.intersects_box(box1, box2)
  local box1 = flib_bounding_box.ensure_explicit(box1)
  local box2 = flib_bounding_box.ensure_explicit(box2)
  return box1.left_top.x < box2.right_bottom.x
    and box2.left_top.x < box1.right_bottom.x
    and box1.left_top.y < box2.right_bottom.y
    and box2.left_top.y < box1.right_bottom.y
end

--- Return a new box with  the same dimensions, moved by the given delta.
--- @param box BoundingBox
--- @param delta MapPosition
--- @return BoundingBox
function flib_bounding_box.move(box, delta)
  local dx = delta.x or delta[1]
  local dy = delta.y or delta[2]
  if box.left_top then
    return {
      left_top = { x = box.left_top.x + dx, y = box.left_top.y + dy },
      right_bottom = { x = box.right_bottom.x + dx, y = box.right_bottom.y + dy },
    }
  else
    return {
      { box[1][1] + dx, box[1][2] + dy },
      { box[2][1] + dx, box[2][2] + dy },
    }
  end
end

--- Return a new box with the same dimensions centered on the given position.
--- @param box BoundingBox
--- @param pos MapPosition
--- @return BoundingBox
function flib_bounding_box.recenter_on(box, pos)
  local height = flib_bounding_box.height(box)
  local width = flib_bounding_box.width(box)

  local pos_x = pos.x or pos[1]
  local pos_y = pos.y or pos[2]

  if box.left_top then
    return {
      left_top = { x = pos_x - (width / 2), y = pos_y - (height / 2) },
      right_bottom = { x = pos_x + (width / 2), y = pos_y + (height / 2) },
    }
  else
    return {
      { pos_x - (width / 2), pos_y - (height / 2) },
      { pos_x + (width / 2), pos_y + (height / 2) },
    }
  end
end

--- Return a new box grown or shrunk by the given delta. A positive delta will grow the box, a negative delta will
--- shrink it.
--- @param box BoundingBox
--- @param delta number
--- @return BoundingBox
function flib_bounding_box.resize(box, delta)
  if box.left_top then
    return {
      left_top = { x = box.left_top.x - delta, y = box.left_top.y - delta },
      right_bottom = { x = box.right_bottom.x + delta, y = box.right_bottom.y + delta },
    }
  else
    return {
      { box[1][1] - delta, box[1][2] - delta },
      { box[2][1] + delta, box[2][2] + delta },
    }
  end
end

--- Return a new box rotated 90 degrees about its center.
--- @param box BoundingBox
--- @return BoundingBox
function flib_bounding_box.rotate(box)
  local center = flib_bounding_box.center(box)
  local radius_x = flib_bounding_box.width(box) / 2
  local radius_y = flib_bounding_box.height(box) / 2

  if box.left_top then
    return {
      left_top = { x = center.x - radius_y, y = center.y - radius_x },
      right_bottom = { x = center.x + radius_y, y = center.y + radius_x },
    }
  else
    return {
      { center.x - radius_y, center.y - radius_x },
      { center.x + radius_y, center.y + radius_x },
    }
  end
end

--- Return a new box expanded to create a square.
--- @param box BoundingBox
--- @return BoundingBox
function flib_bounding_box.square(box)
  local radius = math.max(flib_bounding_box.width(box), flib_bounding_box.height(box))
  return flib_bounding_box.from_dimensions(flib_bounding_box.center(box), radius, radius)
end

--- Calculate the width of the box.
--- @param box BoundingBox
--- @return number
function flib_bounding_box.width(box)
  if box.left_top then
    return box.right_bottom.x - box.left_top.x
  else
    return box[2][1] - box[1][1]
  end
end

return flib_bounding_box
