///|
/// Represents a 2D size with integer width and height.
pub(all) struct Size {
  width : Int32
  height : Int32
} derive(Eq, Debug, Default)

///|
pub fn Size::new(width : Int32, height : Int32) -> Size {
  { width, height, }
}

///|
pub fn Size::area(self : Size) -> Int32 {
  self.width * self.height
}

///|
/// Represents a target bin with width and height boundaries.
pub(all) struct Bin {
  width : Int32
  height : Int32
} derive(Eq, Debug, Default)

///|
pub fn Bin::new(width : Int32, height : Int32) -> Bin {
  { width, height, }
}

///|
pub fn Bin::area(self : Bin) -> Int32 {
  self.width * self.height
}

///|
/// Represents a 2D rectangle with position (x, y), dimensions (width, height),
/// an optional identifier, rotation flag, and bin page index.
pub(all) struct Rect {
  x : Int32
  y : Int32
  width : Int32
  height : Int32
  id : Int32
  rotated : Bool
  bin_index : Int32
} derive(Eq, Debug, Default)

///|
pub fn Rect::new(
  x : Int32,
  y : Int32,
  width : Int32,
  height : Int32,
  id~ : Int32 = 0,
  rotated~ : Bool = false,
  bin_index~ : Int32 = 0,
) -> Rect {
  { x, y, width, height, id, rotated, bin_index, }
}

///|
pub fn Rect::area(self : Rect) -> Int32 {
  self.width * self.height
}

///|
pub fn Rect::right(self : Rect) -> Int32 {
  self.x + self.width
}

///|
pub fn Rect::bottom(self : Rect) -> Int32 {
  self.y + self.height
}

///|
pub fn Rect::perimeter(self : Rect) -> Int32 {
  2 * (self.width + self.height)
}

///|
/// Returns true if this rectangle completely contains `other`.
pub fn Rect::contains(self : Rect, other : Rect) -> Bool {
  self.x <= other.x &&
  self.y <= other.y &&
  self.right() >= other.right() &&
  self.bottom() >= other.bottom()
}

///|
/// Returns true if this rectangle intersects with `other` (strict overlap with non-zero area).
pub fn Rect::intersects(self : Rect, other : Rect) -> Bool {
  self.x < other.right() &&
  self.right() > other.x &&
  self.y < other.bottom() &&
  self.bottom() > other.y
}

///|
/// Computes the intersection rectangle between `self` and `other`, if one exists.
pub fn Rect::intersection(self : Rect, other : Rect) -> Rect? {
  let nx = if self.x > other.x { self.x } else { other.x }
  let ny = if self.y > other.y { self.y } else { other.y }
  let nr = if self.right() < other.right() {
    self.right()
  } else {
    other.right()
  }
  let nb = if self.bottom() < other.bottom() {
    self.bottom()
  } else {
    other.bottom()
  }
  if nr > nx && nb > ny {
    Some(Rect::new(nx, ny, nr - nx, nb - ny))
  } else {
    None
  }
}

///|
/// Normalized UV texture coordinates in [0.0, 1.0] for GPU rendering pipelines.
pub(all) struct UvRect {
  u_min : Double
  v_min : Double
  u_max : Double
  v_max : Double
} derive(Eq, Debug, Default)

///|
pub fn UvRect::new(
  u_min : Double,
  v_min : Double,
  u_max : Double,
  v_max : Double,
) -> UvRect {
  { u_min, v_min, u_max, v_max, }
}

///|
/// Computes normalized UV texture coordinates for `self` within an atlas of (atlas_width, atlas_height).
/// When `half_texel_inset = true`, applies half-texel offset compensation:
/// u_min = (x + 0.5) / atlas_width, u_max = (x + width - 0.5) / atlas_width
/// to strictly prevent bilinear interpolation sampling bleeding across sprite boundaries.
/// When `flip_v = true`, inverts the vertical axis for bottom-left origin coordinate systems (e.g. WebGL/OpenGL).
pub fn Rect::to_uv_ex(
  self : Rect,
  atlas_width : Int32,
  atlas_height : Int32,
  half_texel_inset~ : Bool = true,
  flip_v~ : Bool = false,
) -> UvRect {
  if atlas_width <= 0 || atlas_height <= 0 {
    return Default::default()
  }
  let w_dbl = atlas_width.to_double()
  let h_dbl = atlas_height.to_double()

  let (u0, u1, v0, v1) = if half_texel_inset {
    let u_min = (self.x.to_double() + 0.5) / w_dbl
    let v_min = (self.y.to_double() + 0.5) / h_dbl
    let u_max = (self.right().to_double() - 0.5) / w_dbl
    let v_max = (self.bottom().to_double() - 0.5) / h_dbl
    (u_min, u_max, v_min, v_max)
  } else {
    let u_min = self.x.to_double() / w_dbl
    let v_min = self.y.to_double() / h_dbl
    let u_max = self.right().to_double() / w_dbl
    let v_max = self.bottom().to_double() / h_dbl
    (u_min, u_max, v_min, v_max)
  }

  if flip_v {
    { u_min: u0, v_min: 1.0 - v1, u_max: u1, v_max: 1.0 - v0, }
  } else {
    { u_min: u0, v_min: v0, u_max: u1, v_max: v1, }
  }
}

///|
/// Computes normalized UV texture coordinates for `self` within an atlas of (atlas_width, atlas_height).
/// Applies half-texel offset compensation by default.
pub fn Rect::to_uv(
  self : Rect,
  atlas_width : Int32,
  atlas_height : Int32,
  half_texel_inset~ : Bool = true,
) -> UvRect {
  self.to_uv_ex(atlas_width, atlas_height, half_texel_inset~, flip_v=false)
}

///|
/// Heuristic options for MaxRects bin packing algorithm.
pub(all) enum MaxRectsHeuristic {
  BestShortSideFit
  BestLongSideFit
  BestAreaFit
  BottomLeftRule
  ContactPointRule
} derive(Eq, Debug)

///|
/// Free rectangle choice heuristics for Guillotine bin packing algorithm.
pub(all) enum GuillotineHeuristic {
  BestAreaFit
  BestShortSideFit
  BestLongSideFit
  WorstAreaFit
  WorstShortSideFit
  WorstLongSideFit
} derive(Eq, Debug)

///|
/// Split heuristics for Guillotine cut algorithm.
pub(all) enum SplitHeuristic {
  ShorterLeftoverAxis
  LongerLeftoverAxis
  MinimizeArea
  MaximizeArea
  ShorterAxis
  LongerAxis
} derive(Eq, Debug)

///|
/// Heuristics for Shelf bin packing algorithm.
pub(all) enum ShelfHeuristic {
  NextFit
  FirstFit
  BestAreaFit
  BestHeightFit
  WorstAreaFit
} derive(Eq, Debug)

///|
/// Heuristics for Skyline bin packing algorithm.
pub(all) enum SkylineHeuristic {
  BottomLeft
  MinWasteFit
} derive(Eq, Debug)

///|
/// Packing performance and occupancy statistics.
pub(all) struct PackingStats {
  total_items : Int32
  packed_items : Int32
  unpacked_items : Int32
  total_bin_area : Int32
  used_area : Int32
  occupancy : Double
  bins_used : Int32
} derive(Eq, Debug, Default)

///|
pub fn PackingStats::compute(
  total_items : Int32,
  packed : Array[Rect],
  unpacked : Array[Size],
  bins_used : Int32,
  bin_area : Int32,
) -> PackingStats {
  let mut used_area = 0
  for r in packed {
    used_area = used_area + r.area()
  }
  let total_bin_area = bins_used * bin_area
  let occupancy = if total_bin_area > 0 {
    used_area.to_double() / total_bin_area.to_double()
  } else {
    0.0
  }
  {
    total_items,
    packed_items: packed.length(),
    unpacked_items: unpacked.length(),
    total_bin_area,
    used_area,
    occupancy,
    bins_used,
  }
}