///|
fn scale_permille(numerator : Int, denominator : Int) -> Int {
  if denominator <= 0 {
    0
  } else {
    numerator * 1000 / denominator
  }
}

///|
fn non_negative(value : Int) -> Int {
  if value < 0 {
    0
  } else {
    value
  }
}

///|
fn padded_capacity_for_options(options : PackOptions) -> Int {
  let width = non_negative(options.bin.width - options.padding.outer * 2)
  let height = non_negative(options.bin.height - options.padding.outer * 2)
  width * height
}

///|
fn rect_fits_bin(rect : Rect, bin : Bin) -> Bool {
  if bin.width <= 0 || bin.height <= 0 {
    true
  } else {
    rect.width <= bin.width && rect.height <= bin.height
  }
}

///|
fn empty_rect_stats() -> RectSetStats {
  RectSetStats::{
    count: 0,
    total_area: 0,
    min_width: 0,
    max_width: 0,
    min_height: 0,
    max_height: 0,
    min_area: 0,
    max_area: 0,
    average_width_permille: 0,
    average_height_permille: 0,
    average_area_permille: 0,
    square_count: 0,
    wide_count: 0,
    tall_count: 0,
    oversized_count: 0,
  }
}

///|
fn summarize_rects_inner(rects : ArrayView[Rect], bin : Bin) -> RectSetStats {
  if rects.is_empty() {
    return empty_rect_stats()
  }
  let first = rects[0]
  let mut total_area = 0
  let mut total_width = 0
  let mut total_height = 0
  let mut min_width = first.width
  let mut max_width = first.width
  let mut min_height = first.height
  let mut max_height = first.height
  let mut min_area = rect_area(first)
  let mut max_area = rect_area(first)
  let mut square_count = 0
  let mut wide_count = 0
  let mut tall_count = 0
  let mut oversized_count = 0
  for rect in rects {
    let area = rect_area(rect)
    total_area = total_area + area
    total_width = total_width + rect.width
    total_height = total_height + rect.height
    if rect.width < min_width {
      min_width = rect.width
    }
    if rect.width > max_width {
      max_width = rect.width
    }
    if rect.height < min_height {
      min_height = rect.height
    }
    if rect.height > max_height {
      max_height = rect.height
    }
    if area < min_area {
      min_area = area
    }
    if area > max_area {
      max_area = area
    }
    if rect.width == rect.height {
      square_count = square_count + 1
    } else if rect.width > rect.height {
      wide_count = wide_count + 1
    } else {
      tall_count = tall_count + 1
    }
    if !rect_fits_bin(rect, bin) {
      oversized_count = oversized_count + 1
    }
  }
  let count = rects.length()
  RectSetStats::{
    count,
    total_area,
    min_width,
    max_width,
    min_height,
    max_height,
    min_area,
    max_area,
    average_width_permille: scale_permille(total_width, count),
    average_height_permille: scale_permille(total_height, count),
    average_area_permille: scale_permille(total_area, count),
    square_count,
    wide_count,
    tall_count,
    oversized_count,
  }
}

///|
/// Summarize a rectangle collection without considering a target bin.
pub fn summarize_rects(rects : ArrayView[Rect]) -> RectSetStats {
  summarize_rects_inner(rects, Bin::{ width: 0, height: 0 })
}

///|
/// Summarize a rectangle collection and count items larger than `bin`.
pub fn summarize_rects_for_bin(
  rects : ArrayView[Rect],
  bin : Bin,
) -> RectSetStats {
  summarize_rects_inner(rects, bin)
}

///|
fn min_placed_x(placements : ArrayView[Placed]) -> Int {
  if placements.is_empty() {
    return 0
  }
  let mut value = placements[0].x
  for placed in placements {
    if placed.x < value {
      value = placed.x
    }
  }
  value
}

///|
fn min_placed_y(placements : ArrayView[Placed]) -> Int {
  if placements.is_empty() {
    return 0
  }
  let mut value = placements[0].y
  for placed in placements {
    if placed.y < value {
      value = placed.y
    }
  }
  value
}

///|
fn max_placed_right(placements : ArrayView[Placed]) -> Int {
  if placements.is_empty() {
    return 0
  }
  let mut value = placements[0].x + placements[0].width
  for placed in placements {
    let right = placed.x + placed.width
    if right > value {
      value = right
    }
  }
  value
}

///|
fn max_placed_bottom(placements : ArrayView[Placed]) -> Int {
  if placements.is_empty() {
    return 0
  }
  let mut value = placements[0].y + placements[0].height
  for placed in placements {
    let bottom = placed.y + placed.height
    if bottom > value {
      value = bottom
    }
  }
  value
}

///|
fn rotated_count(placements : ArrayView[Placed]) -> Int {
  let mut count = 0
  for placed in placements {
    if placed.rotated {
      count = count + 1
    }
  }
  count
}

///|
fn row_seen_before(placements : ArrayView[Placed], index : Int) -> Bool {
  let y = placements[index].y
  for i in 0.. Bool {
  let x = placements[index].x
  for i in 0.. Int {
  let mut count = 0
  for i in 0.. Int {
  let mut count = 0
  for i in 0.. Int {
  if placements.is_empty() {
    return 0
  }
  let rows = unique_row_count(placements)
  scale_permille(placements.length(), rows.max(1))
}

///|
fn column_density_score(placements : ArrayView[Placed]) -> Int {
  if placements.is_empty() {
    return 0
  }
  let cols = unique_column_count(placements)
  scale_permille(placements.length(), cols.max(1))
}

///|
fn fragmentation_from_rows_cols(rows : Int, cols : Int, placed : Int) -> Int {
  if rows <= 0 || cols <= 0 || placed <= 0 {
    return 0
  }
  let slots = rows * cols
  if slots <= placed {
    0
  } else {
    scale_permille(slots - placed, slots)
  }
}

///|
fn compactness_from_stats(
  used_area : Int,
  bounding_area : Int,
  occupancy : Int,
) -> Int {
  if bounding_area <= 0 {
    occupancy
  } else {
    scale_permille(used_area, bounding_area)
  }
}

///|
fn empty_layout_stats(layout : Layout, padded_capacity : Int) -> LayoutStats {
  LayoutStats::{
    placed_count: 0,
    unpacked_count: layout.unpacked.length(),
    rotated_count: 0,
    used_area: 0,
    container_area: bin_area(layout.bin),
    padded_capacity,
    occupancy_permille: 0,
    padded_occupancy_permille: 0,
    waste_area: bin_area(layout.bin),
    bounding_x: 0,
    bounding_y: 0,
    bounding_width: 0,
    bounding_height: 0,
    bounding_area: 0,
    bounding_occupancy_permille: 0,
    row_count: 0,
    column_count: 0,
    horizontal_span: 0,
    vertical_span: 0,
    fragmentation_score: 0,
    compactness_score: 0,
  }
}

///|
fn analyze_layout_inner(
  layout : Layout,
  padded_capacity : Int,
) -> Result[LayoutStats, PackError] {
  match validate_layout(layout) {
    Err(err) => return Err(err)
    Ok(_) => ()
  }
  if layout.placements.is_empty() {
    return Ok(empty_layout_stats(layout, padded_capacity))
  }
  let min_x = min_placed_x(layout.placements)
  let min_y = min_placed_y(layout.placements)
  let max_x = max_placed_right(layout.placements)
  let max_y = max_placed_bottom(layout.placements)
  let bounding_width = max_x - min_x
  let bounding_height = max_y - min_y
  let bounding_area = bounding_width * bounding_height
  let used_area = total_placed_area(layout.placements)
  let container_area = bin_area(layout.bin)
  let rows = unique_row_count(layout.placements)
  let cols = unique_column_count(layout.placements)
  let occupancy = scale_permille(used_area, container_area)
  let padded_occupancy = scale_permille(used_area, padded_capacity)
  let bounding_occupancy = scale_permille(used_area, bounding_area)
  let fragmentation = fragmentation_from_rows_cols(
    rows,
    cols,
    layout.placements.length(),
  )
  LayoutStats::{
    placed_count: layout.placements.length(),
    unpacked_count: layout.unpacked.length(),
    rotated_count: rotated_count(layout.placements),
    used_area,
    container_area,
    padded_capacity,
    occupancy_permille: occupancy,
    padded_occupancy_permille: padded_occupancy,
    waste_area: container_area - used_area,
    bounding_x: min_x,
    bounding_y: min_y,
    bounding_width,
    bounding_height,
    bounding_area,
    bounding_occupancy_permille: bounding_occupancy,
    row_count: rows,
    column_count: cols,
    horizontal_span: bounding_width,
    vertical_span: bounding_height,
    fragmentation_score: fragmentation,
    compactness_score: compactness_from_stats(
      used_area, bounding_area, occupancy,
    ),
  }
  |> Ok
}

///|
/// Analyze a layout using the full container area as capacity.
pub fn analyze_layout(layout : Layout) -> Result[LayoutStats, PackError] {
  analyze_layout_inner(layout, bin_area(layout.bin))
}

///|
/// Analyze a layout with the padded capacity from the original options.
pub fn analyze_layout_with_options(
  layout : Layout,
  options : PackOptions,
) -> Result[LayoutStats, PackError] {
  analyze_layout_inner(layout, padded_capacity_for_options(options))
}

///|
pub fn layout_quality_score(stats : LayoutStats) -> Int {
  let pack_bonus = stats.placed_count * 10000
  let unpack_penalty = stats.unpacked_count * 12000
  let occupancy_bonus = stats.occupancy_permille * 8
  let compact_bonus = stats.compactness_score * 2
  let fragmentation_penalty = stats.fragmentation_score * 2
  pack_bonus +
  occupancy_bonus +
  compact_bonus -
  unpack_penalty -
  fragmentation_penalty
}

///|
pub fn utilization_label(permille : Int) -> String {
  if permille >= 900 {
    "excellent"
  } else if permille >= 750 {
    "strong"
  } else if permille >= 550 {
    "usable"
  } else if permille >= 350 {
    "sparse"
  } else {
    "low"
  }
}

///|
fn write_stat_line(out : StringBuilder, label : String, value : Int) -> Unit {
  out.write_string(label)
  out.write_string(": ")
  out.write_string("\{value}")
  out.write_char('\n')
}

///|
fn write_permille_line(
  out : StringBuilder,
  label : String,
  value : Int,
) -> Unit {
  out.write_string(label)
  out.write_string(": ")
  out.write_string("\{value}/1000 ")
  out.write_string(utilization_label(value))
  out.write_char('\n')
}

///|
pub fn to_rect_stats_report(stats : RectSetStats) -> String {
  let out = StringBuilder()
  out.write_string("rectloom rect-set statistics\n")
  write_stat_line(out, "count", stats.count)
  write_stat_line(out, "total_area", stats.total_area)
  write_stat_line(out, "min_width", stats.min_width)
  write_stat_line(out, "max_width", stats.max_width)
  write_stat_line(out, "min_height", stats.min_height)
  write_stat_line(out, "max_height", stats.max_height)
  write_stat_line(out, "min_area", stats.min_area)
  write_stat_line(out, "max_area", stats.max_area)
  write_stat_line(out, "average_width_x1000", stats.average_width_permille)
  write_stat_line(out, "average_height_x1000", stats.average_height_permille)
  write_stat_line(out, "average_area_x1000", stats.average_area_permille)
  write_stat_line(out, "square_count", stats.square_count)
  write_stat_line(out, "wide_count", stats.wide_count)
  write_stat_line(out, "tall_count", stats.tall_count)
  write_stat_line(out, "oversized_count", stats.oversized_count)
  out.to_string()
}

///|
pub fn to_layout_stats_report(stats : LayoutStats) -> String {
  let out = StringBuilder()
  out.write_string("rectloom layout statistics\n")
  write_stat_line(out, "placed_count", stats.placed_count)
  write_stat_line(out, "unpacked_count", stats.unpacked_count)
  write_stat_line(out, "rotated_count", stats.rotated_count)
  write_stat_line(out, "used_area", stats.used_area)
  write_stat_line(out, "container_area", stats.container_area)
  write_stat_line(out, "padded_capacity", stats.padded_capacity)
  write_permille_line(out, "occupancy", stats.occupancy_permille)
  write_permille_line(out, "padded_occupancy", stats.padded_occupancy_permille)
  write_stat_line(out, "waste_area", stats.waste_area)
  write_stat_line(out, "bounding_x", stats.bounding_x)
  write_stat_line(out, "bounding_y", stats.bounding_y)
  write_stat_line(out, "bounding_width", stats.bounding_width)
  write_stat_line(out, "bounding_height", stats.bounding_height)
  write_stat_line(out, "bounding_area", stats.bounding_area)
  write_permille_line(
    out,
    "bounding_occupancy",
    stats.bounding_occupancy_permille,
  )
  write_stat_line(out, "row_count", stats.row_count)
  write_stat_line(out, "column_count", stats.column_count)
  write_stat_line(out, "horizontal_span", stats.horizontal_span)
  write_stat_line(out, "vertical_span", stats.vertical_span)
  write_stat_line(out, "fragmentation_score", stats.fragmentation_score)
  write_stat_line(out, "compactness_score", stats.compactness_score)
  write_stat_line(out, "quality_score", layout_quality_score(stats))
  out.to_string()
}

///|
pub fn to_analysis_report(layout : Layout) -> Result[String, PackError] {
  match analyze_layout(layout) {
    Err(err) => Err(err)
    Ok(stats) => Ok(to_layout_stats_report(stats))
  }
}

///|
pub fn row_density_hint(layout : Layout) -> Result[Int, PackError] {
  match validate_layout(layout) {
    Err(err) => Err(err)
    Ok(_) => Ok(row_density_score(layout.placements))
  }
}

///|
pub fn column_density_hint(layout : Layout) -> Result[Int, PackError] {
  match validate_layout(layout) {
    Err(err) => Err(err)
    Ok(_) => Ok(column_density_score(layout.placements))
  }
}