if ... ~= "__flib__.position" then
  return require("__flib__.position")
end

local flib_math = require("__flib__.math")

--- Utilities for manipulating positions. All functions support both the shorthand and explicit syntaxes and will
--- preserve the syntax that was passed in.
--- ```lua
--- local flib_position = require("__flib__.position")
--- ```
--- @class flib_position
local flib_position = {}

--- FIXME: Sumneko doesn't properly handle generics yet and throws a bunch of bogus warnings.
--- @diagnostic disable

--- Return the absolute value of the position's coordinates.
--- @generic P
--- @param pos P
--- @return P
function flib_position.abs(pos)
  if pos.x then
    return { x = math.abs(pos.x), y = math.abs(pos.y) }
  else
    return { math.abs(pos[1]), math.abs(pos[2]) }
  end
end

--- Add two positions.
--- @generic P
--- @param pos1 P
--- @param pos2 P
--- @return P
function flib_position.add(pos1, pos2)
  local x1 = pos1.x or pos1[1]
  local y1 = pos1.y or pos1[2]
  local x2 = pos2.x or pos2[1]
  local y2 = pos2.y or pos2[2]
  if pos1.x then
    return { x = x1 + x2, y = y1 + y2 }
  else
    return { x1 + x2, y1 + y2 }
  end
end

--- Ceil the given position.
--- @generic P
--- @param pos P
--- @return P
function flib_position.ceil(pos)
  if pos.x then
    return { x = math.ceil(pos.x), y = math.ceil(pos.y) }
  else
    return { math.ceil(pos[1]), math.ceil(pos[2]) }
  end
end

--- Calculate the distance between two positions.
--- @generic P
--- @param pos1 P
--- @param pos2 P
--- @return number
function flib_position.distance(pos1, pos2)
  local x1 = pos1.x or pos1[1]
  local y1 = pos1.y or pos1[2]
  local x2 = pos2.x or pos2[1]
  local y2 = pos2.y or pos2[2]
  return math.sqrt((x1 - x2) ^ 2 + (y1 - y2) ^ 2)
end

--- Calculate the squared distance between two positions.
--- @generic P
--- @param pos1 P
--- @param pos2 P
--- @return number
function flib_position.distance_squared(pos1, pos2)
  local x1 = pos1.x or pos1[1]
  local y1 = pos1.y or pos1[2]
  local x2 = pos2.x or pos2[1]
  local y2 = pos2.y or pos2[2]
  return (x1 - x2) ^ 2 + (y1 - y2) ^ 2
end

--- Divide two positions.
--- @generic P
--- @param pos1 P
--- @param pos2 P
--- @return P
function flib_position.div(pos1, pos2)
  local x1 = pos1.x or pos1[1]
  local y1 = pos1.y or pos1[2]
  local x2 = pos2.x or pos2[1]
  local y2 = pos2.y or pos2[2]
  if pos1.x then
    return { x = x1 / x2, y = y1 / y2 }
  else
    return { x1 / x2, y1 / y2 }
  end
end

--- Return the position in explicit form.
--- @generic P
--- @param pos P
--- @return P
function flib_position.ensure_explicit(pos)
  if pos.x then
    return pos
  else
    return { x = pos[1], y = pos[2] }
  end
end

--- Return the  position in shorthand form.
--- @generic P
--- @param pos P
--- @return P
function flib_position.ensure_short(pos)
  if pos.x then
    return { pos.x, pos.y }
  else
    return pos
  end
end

--- Test if two positions are equal.
--- @generic P
--- @param pos1 P
--- @param pos2 P
--- @return boolean
function flib_position.eq(pos1, pos2)
  local x1 = pos1.x or pos1[1]
  local y1 = pos1.y or pos1[2]
  local x2 = pos2.x or pos2[1]
  local y2 = pos2.y or pos2[2]
  return x1 == x2 and y1 == y2
end

--- Floor the given position.
--- @generic P
--- @param pos P
--- @return P
function flib_position.floor(pos)
  if pos.x then
    return { x = math.floor(pos.x), y = math.floor(pos.y) }
  else
    return { math.floor(pos[1]), math.floor(pos[2]) }
  end
end

--- Convert a `ChunkPosition` into a `TilePosition` by multiplying by 32.
--- @param pos ChunkPosition
--- @return TilePosition
function flib_position.from_chunk(pos)
  if pos.x then
    return { x = pos.x * 32, y = pos.y * 32 }
  else
    return { pos[1] * 32, pos[2] * 32 }
  end
end

--- Test if `pos1` is greater than or equal to `pos2`.
--- @generic P
--- @param pos1 P
--- @param pos2 P
--- @return boolean
function flib_position.ge(pos1, pos2)
  local x1 = pos1.x or pos1[1]
  local y1 = pos1.y or pos1[2]
  local x2 = pos2.x or pos2[1]
  local y2 = pos2.y or pos2[2]
  return x1 >= x2 and y1 >= y2
end

--- Test if `pos1` is greater than `pos2`.
--- @generic P
--- @param pos1 P
--- @param pos2 P
--- @return boolean
function flib_position.gt(pos1, pos2)
  local x1 = pos1.x or pos1[1]
  local y1 = pos1.y or pos1[2]
  local x2 = pos2.x or pos2[1]
  local y2 = pos2.y or pos2[2]
  return x1 > x2 and y1 > y2
end

--- Test if `pos1` is less than or equal to `pos2`.
--- @generic P
--- @param pos1 P
--- @param pos2 P
--- @return boolean
function flib_position.le(pos1, pos2)
  local x1 = pos1.x or pos1[1]
  local y1 = pos1.y or pos1[2]
  local x2 = pos2.x or pos2[1]
  local y2 = pos2.y or pos2[2]
  return x1 <= x2 and y1 <= y2
end

--- Linearly interpolate between two positions. For example, an amount of 0.5 will return the midpoint.
--- @generic P
--- @param pos1 P
--- @param pos2 P
--- @param amount number
--- @return P
function flib_position.lerp(pos1, pos2, amount)
  if pos1.x then
    return { x = flib_math.lerp(pos1.x, pos2.x, amount), y = flib_math.lerp(pos1.y, pos2.y, amount) }
  else
    return { flib_math.lerp(pos1[1], pos2[1], amount), flib_math.lerp(pos1[2], pos2[2], amount) }
  end
end

--- Test if `pos1` is less than `pos2`.
--- @generic P
--- @param pos1 P
--- @param pos2 P
--- @return boolean
function flib_position.lt(pos1, pos2)
  local x1 = pos1.x or pos1[1]
  local y1 = pos1.y or pos1[2]
  local x2 = pos2.x or pos2[1]
  local y2 = pos2.y or pos2[2]
  return x1 < x2 and y1 < y2
end

--- Take the remainder (modulus) of two positions.
--- @generic P
--- @param pos1 P
--- @param pos2 P
--- @return P
function flib_position.mod(pos1, pos2)
  local x1 = pos1.x or pos1[1]
  local y1 = pos1.y or pos1[2]
  local x2 = pos2.x or pos2[1]
  local y2 = pos2.y or pos2[2]
  if pos1.x then
    return { x = x1 % x2, y = y1 % y2 }
  else
    return { x1 % x2, y1 % y2 }
  end
end

--- Multiply two positions.
--- @generic P
--- @param pos1 P
--- @param pos2 P
--- @return P
function flib_position.mul(pos1, pos2)
  local x1 = pos1.x or pos1[1]
  local y1 = pos1.y or pos1[2]
  local x2 = pos2.x or pos2[1]
  local y2 = pos2.y or pos2[2]
  if pos1.x then
    return { x = x1 * x2, y = y1 * y2 }
  else
    return { x1 * x2, y1 * y2 }
  end
end

--- Subtract two positions.
--- @generic P
--- @param pos1 P
--- @param pos2 P
--- @return P
function flib_position.sub(pos1, pos2)
  local x1 = pos1.x or pos1[1]
  local y1 = pos1.y or pos1[2]
  local x2 = pos2.x or pos2[1]
  local y2 = pos2.y or pos2[2]
  if pos1.x then
    return { x = x1 - x2, y = y1 - y2 }
  else
    return { x1 - x2, y1 - y2 }
  end
end

--- Take the power of two positions. `pos1^pos2`.
--- @generic P
--- @param pos1 P
--- @param pos2 P
--- @return P
function flib_position.pow(pos1, pos2)
  local x1 = pos1.x or pos1[1]
  local y1 = pos1.y or pos1[2]
  local x2 = pos2.x or pos2[1]
  local y2 = pos2.y or pos2[2]
  if pos1.x then
    return { x = x1 ^ x2, y = y1 ^ y2 }
  else
    return { x1 ^ x2, y1 ^ y2 }
  end
end

--- Convert a `MapPosition` or `TilePosition` into a `ChunkPosition` by dividing by 32 and flooring.
--- @param pos MapPosition|TilePosition
--- @return ChunkPosition
function flib_position.to_chunk(pos)
  if pos.x then
    return { x = math.floor(pos.x / 32), y = math.floor(pos.y / 32) }
  else
    return { math.floor(pos[1] / 32), math.floor(pos[2] / 32) }
  end
end

--- Convert a `MapPosition` into a `TilePosition` by flooring.
--- @param pos MapPosition
--- @return TilePosition
function flib_position.to_tile(pos)
  if pos.x then
    return { x = math.floor(pos.x), y = math.floor(pos.y) }
  else
    return { math.floor(pos[1]), math.floor(pos[2]) }
  end
end

return flib_position
