///|
fn scaled_value(value : Int, permille : Int) -> Int {
  let scaled = value * permille / 1000
  if scaled <= 0 {
    1
  } else {
    scaled
  }
}

///|
fn rect_with_size(rect : Rect, width : Int, height : Int) -> Rect {
  Rect::{ id: rect.id, width, height }
}

///|
fn rect_with_id(rect : Rect, id : String) -> Rect {
  Rect::{ id, width: rect.width, height: rect.height }
}

///|
/// Scale all rectangles by an integer permille factor.
pub fn scale_rects(rects : ArrayView[Rect], permille : Int) -> Array[Rect] {
  let out : Array[Rect] = []
  for rect in rects {
    out.push(
      rect_with_size(
        rect,
        scaled_value(rect.width, permille),
        scaled_value(rect.height, permille),
      ),
    )
  }
  out
}

///|
/// Add pixels around every rectangle.
pub fn expand_rects(rects : ArrayView[Rect], amount : Int) -> Array[Rect] {
  let out : Array[Rect] = []
  let delta = amount.max(0) * 2
  for rect in rects {
    out.push(rect_with_size(rect, rect.width + delta, rect.height + delta))
  }
  out
}

///|
/// Remove pixels around every rectangle while keeping each side positive.
pub fn shrink_rects(rects : ArrayView[Rect], amount : Int) -> Array[Rect] {
  let out : Array[Rect] = []
  let delta = amount.max(0) * 2
  for rect in rects {
    out.push(
      rect_with_size(
        rect,
        (rect.width - delta).max(1),
        (rect.height - delta).max(1),
      ),
    )
  }
  out
}

///|
/// Prefix all rectangle ids.
pub fn prefix_rect_ids(rects : ArrayView[Rect], prefix : String) -> Array[Rect] {
  let out : Array[Rect] = []
  for rect in rects {
    out.push(rect_with_id(rect, "\{prefix}\{rect.id}"))
  }
  out
}

///|
/// Suffix all rectangle ids.
pub fn suffix_rect_ids(rects : ArrayView[Rect], suffix : String) -> Array[Rect] {
  let out : Array[Rect] = []
  for rect in rects {
    out.push(rect_with_id(rect, "\{rect.id}\{suffix}"))
  }
  out
}

///|
fn rect_can_fit_direct(rect : Rect, bin : Bin) -> Bool {
  rect.width <= bin.width && rect.height <= bin.height
}

///|
fn rect_can_fit_rotated(rect : Rect, bin : Bin) -> Bool {
  rect.height <= bin.width && rect.width <= bin.height
}

///|
/// Keep rectangles that can fit the bin, optionally considering rotation.
pub fn filter_fit_rects(
  rects : ArrayView[Rect],
  bin : Bin,
  allow_rotate : Bool,
) -> Array[Rect] {
  let out : Array[Rect] = []
  for rect in rects {
    if rect_can_fit_direct(rect, bin) ||
      (allow_rotate && rect_can_fit_rotated(rect, bin)) {
      out.push(rect)
    }
  }
  out
}

///|
/// Keep rectangles that cannot fit the bin.
pub fn filter_oversized_rects(
  rects : ArrayView[Rect],
  bin : Bin,
  allow_rotate : Bool,
) -> Array[Rect] {
  let out : Array[Rect] = []
  for rect in rects {
    if !rect_can_fit_direct(rect, bin) &&
      !(allow_rotate && rect_can_fit_rotated(rect, bin)) {
      out.push(rect)
    }
  }
  out
}

///|
fn clamp_dimension(value : Int, limit : Int) -> Int {
  if limit <= 0 {
    value.max(1)
  } else {
    value.min(limit).max(1)
  }
}

///|
/// Clamp rectangle dimensions to a bin-sized maximum.
pub fn clamp_rects_to_bin(rects : ArrayView[Rect], bin : Bin) -> Array[Rect] {
  let out : Array[Rect] = []
  for rect in rects {
    out.push(
      rect_with_size(
        rect,
        clamp_dimension(rect.width, bin.width),
        clamp_dimension(rect.height, bin.height),
      ),
    )
  }
  out
}

///|
fn split_count_for(value : Int, limit : Int) -> Int {
  if limit <= 0 || value <= limit {
    1
  } else {
    ceil_div(value, limit)
  }
}

///|
fn split_width_once(rect : Rect, max_width : Int) -> Array[Rect] {
  let out : Array[Rect] = []
  let parts = split_count_for(rect.width, max_width)
  if parts <= 1 {
    out.push(rect)
    return out
  }
  let mut remaining = rect.width
  for i in 0.. max_width { max_width } else { remaining }
    out.push(Rect::make("\{rect.id}-w\{i}", width, rect.height))
    remaining = remaining - width
  }
  out
}

///|
/// Split rectangles wider than `max_width` into deterministic pieces.
pub fn split_wide_rects(
  rects : ArrayView[Rect],
  max_width : Int,
) -> Array[Rect] {
  let out : Array[Rect] = []
  for rect in rects {
    for part in split_width_once(rect, max_width) {
      out.push(part)
    }
  }
  out
}

///|
fn split_height_once(rect : Rect, max_height : Int) -> Array[Rect] {
  let out : Array[Rect] = []
  let parts = split_count_for(rect.height, max_height)
  if parts <= 1 {
    out.push(rect)
    return out
  }
  let mut remaining = rect.height
  for i in 0.. max_height { max_height } else { remaining }
    out.push(Rect::make("\{rect.id}-h\{i}", rect.width, height))
    remaining = remaining - height
  }
  out
}

///|
/// Split rectangles taller than `max_height` into deterministic pieces.
pub fn split_tall_rects(
  rects : ArrayView[Rect],
  max_height : Int,
) -> Array[Rect] {
  let out : Array[Rect] = []
  for rect in rects {
    for part in split_height_once(rect, max_height) {
      out.push(part)
    }
  }
  out
}

///|
/// Convert placements back to rectangle dimensions for follow-up packing.
pub fn rects_from_placements(placements : ArrayView[Placed]) -> Array[Rect] {
  let out : Array[Rect] = []
  for placed in placements {
    out.push(Rect::make(placed.id, placed.width, placed.height))
  }
  out
}

///|
/// Convert a layout back to the dimensions that were actually packed.
pub fn rects_from_layout(layout : Layout) -> Array[Rect] {
  rects_from_placements(layout.placements)
}

///|
/// Return all original dimensions from a layout, including unpacked items.
pub fn original_rects_from_layout(layout : Layout) -> Array[Rect] {
  let out : Array[Rect] = []
  for placed in layout.placements {
    out.push(
      Rect::make(placed.id, placed.original_width, placed.original_height),
    )
  }
  for rect in layout.unpacked {
    out.push(rect)
  }
  out
}

///|
fn id_used(rects : ArrayView[Rect], id : String) -> Bool {
  for rect in rects {
    if rect.id == id {
      return true
    }
  }
  false
}

///|
fn next_deduped_id(used : ArrayView[Rect], base : String) -> String {
  let mut suffix = 2
  let mut candidate = "\{base}-dup\{suffix}"
  while id_used(used, candidate) {
    suffix = suffix + 1
    candidate = "\{base}-dup\{suffix}"
  }
  candidate
}

///|
/// Make all ids unique with stable numeric suffixes.
pub fn dedupe_rect_ids(rects : ArrayView[Rect]) -> Array[Rect] {
  let out : Array[Rect] = []
  for rect in rects {
    if id_used(out, rect.id) {
      out.push(rect_with_id(rect, next_deduped_id(out, rect.id)))
    } else {
      out.push(rect)
    }
  }
  out
}

///|
/// Normalize inputs by deduping ids and splitting pieces larger than limits.
pub fn normalize_rects(
  rects : ArrayView[Rect],
  max_width : Int,
  max_height : Int,
) -> Array[Rect] {
  let deduped = dedupe_rect_ids(rects)
  let width_split = split_wide_rects(deduped, max_width)
  split_tall_rects(width_split, max_height)
}

///|
/// Normalize inputs for a target bin.
pub fn normalize_rects_for_bin(
  rects : ArrayView[Rect],
  bin : Bin,
) -> Array[Rect] {
  normalize_rects(rects, bin.width.max(1), bin.height.max(1))
}

///|
fn write_rect_csv_line(out : StringBuilder, rect : Rect) -> Unit {
  out.write_string(csv_escape(rect.id))
  out.write_char(',')
  out.write_string("\{rect.width}")
  out.write_char(',')
  out.write_string("\{rect.height}")
  out.write_char(',')
  out.write_string("\{rect_area(rect)}")
  out.write_char('\n')
}

///|
/// Export rectangle dimensions as CSV.
pub fn rects_to_csv(rects : ArrayView[Rect]) -> String {
  let out = StringBuilder()
  out.write_string("id,width,height,area\n")
  for rect in rects {
    write_rect_csv_line(out, rect)
  }
  out.to_string()
}

///|
fn write_rect_markdown_row(out : StringBuilder, rect : Rect) -> Unit {
  out.write_string("| ")
  out.write_string(rect.id)
  out.write_string(" | ")
  out.write_string("\{rect.width}")
  out.write_string(" | ")
  out.write_string("\{rect.height}")
  out.write_string(" | ")
  out.write_string("\{rect_area(rect)}")
  out.write_string(" |\n")
}

///|
/// Export rectangle dimensions as a Markdown table.
pub fn rects_to_markdown_table(rects : ArrayView[Rect]) -> String {
  let out = StringBuilder()
  out.write_string("| id | width | height | area |\n")
  out.write_string("| --- | ---: | ---: | ---: |\n")
  for rect in rects {
    write_rect_markdown_row(out, rect)
  }
  out.to_string()
}