///|
pub fn LayoutConstraints::default() -> LayoutConstraints {
  LayoutConstraints::{
    min_occupancy_permille: 0,
    max_waste_permille: 1000,
    min_gap: 0,
    max_row_count: 0,
    max_column_count: 0,
    max_rotated_permille: 1000,
    require_all_packed: false,
  }
}

///|
pub fn LayoutConstraints::strict() -> LayoutConstraints {
  LayoutConstraints::{
    min_occupancy_permille: 500,
    max_waste_permille: 500,
    min_gap: 0,
    max_row_count: 0,
    max_column_count: 0,
    max_rotated_permille: 700,
    require_all_packed: true,
  }
}

///|
fn make_violation(
  name : String,
  expected : Int,
  actual : Int,
  severity : IssueSeverity,
) -> ConstraintViolation {
  ConstraintViolation::{ name, expected, actual, severity }
}

///|
fn push_violation(
  violations : Array[ConstraintViolation],
  name : String,
  expected : Int,
  actual : Int,
  severity : IssueSeverity,
) -> Unit {
  violations.push(make_violation(name, expected, actual, severity))
}

///|
fn horizontal_gap(a : Placed, b : Placed) -> Int {
  if a.x + a.width <= b.x {
    b.x - (a.x + a.width)
  } else if b.x + b.width <= a.x {
    a.x - (b.x + b.width)
  } else {
    0
  }
}

///|
fn vertical_gap(a : Placed, b : Placed) -> Int {
  if a.y + a.height <= b.y {
    b.y - (a.y + a.height)
  } else if b.y + b.height <= a.y {
    a.y - (b.y + b.height)
  } else {
    0
  }
}

///|
fn placement_gap(a : Placed, b : Placed) -> Int {
  let x_gap = horizontal_gap(a, b)
  let y_gap = vertical_gap(a, b)
  if x_gap == 0 {
    y_gap
  } else if y_gap == 0 {
    x_gap
  } else {
    grid_min(x_gap, y_gap)
  }
}

///|
fn min_gap_in_layout(placements : ArrayView[Placed]) -> Int {
  if placements.length() < 2 {
    return 0
  }
  let mut best = placement_gap(placements[0], placements[1])
  for i in 0.. Unit {
  if constraints.require_all_packed && !layout.unpacked.is_empty() {
    push_violation(
      violations,
      "require_all_packed",
      0,
      layout.unpacked.length(),
      Critical,
    )
  }
}

///|
fn check_occupancy_constraint(
  violations : Array[ConstraintViolation],
  stats : LayoutStats,
  constraints : LayoutConstraints,
) -> Unit {
  if constraints.min_occupancy_permille > 0 &&
    stats.occupancy_permille < constraints.min_occupancy_permille {
    push_violation(
      violations,
      "min_occupancy_permille",
      constraints.min_occupancy_permille,
      stats.occupancy_permille,
      Warning,
    )
  }
}

///|
fn check_waste_constraint(
  violations : Array[ConstraintViolation],
  stats : LayoutStats,
  constraints : LayoutConstraints,
) -> Unit {
  let waste_permille = scale_permille(stats.waste_area, stats.container_area)
  if constraints.max_waste_permille < 1000 &&
    waste_permille > constraints.max_waste_permille {
    push_violation(
      violations,
      "max_waste_permille",
      constraints.max_waste_permille,
      waste_permille,
      Warning,
    )
  }
}

///|
fn check_gap_constraint(
  violations : Array[ConstraintViolation],
  layout : Layout,
  constraints : LayoutConstraints,
) -> Unit {
  if constraints.min_gap <= 0 || layout.placements.length() < 2 {
    return
  }
  let actual = min_gap_in_layout(layout.placements)
  if actual < constraints.min_gap {
    push_violation(violations, "min_gap", constraints.min_gap, actual, Warning)
  }
}

///|
fn check_row_column_constraints(
  violations : Array[ConstraintViolation],
  stats : LayoutStats,
  constraints : LayoutConstraints,
) -> Unit {
  if constraints.max_row_count > 0 &&
    stats.row_count > constraints.max_row_count {
    push_violation(
      violations,
      "max_row_count",
      constraints.max_row_count,
      stats.row_count,
      Warning,
    )
  }
  if constraints.max_column_count > 0 &&
    stats.column_count > constraints.max_column_count {
    push_violation(
      violations,
      "max_column_count",
      constraints.max_column_count,
      stats.column_count,
      Warning,
    )
  }
}

///|
fn check_rotation_constraint(
  violations : Array[ConstraintViolation],
  stats : LayoutStats,
  constraints : LayoutConstraints,
) -> Unit {
  if constraints.max_rotated_permille >= 1000 || stats.placed_count == 0 {
    return
  }
  let actual = scale_permille(stats.rotated_count, stats.placed_count)
  if actual > constraints.max_rotated_permille {
    push_violation(
      violations,
      "max_rotated_permille",
      constraints.max_rotated_permille,
      actual,
      Warning,
    )
  }
}

///|
/// Check a layout against caller-supplied production constraints.
pub fn check_constraints(
  layout : Layout,
  constraints : LayoutConstraints,
) -> Result[Array[ConstraintViolation], PackError] {
  match analyze_layout(layout) {
    Err(err) => Err(err)
    Ok(stats) => {
      let violations : Array[ConstraintViolation] = []
      check_all_packed(violations, layout, constraints)
      check_occupancy_constraint(violations, stats, constraints)
      check_waste_constraint(violations, stats, constraints)
      check_gap_constraint(violations, layout, constraints)
      check_row_column_constraints(violations, stats, constraints)
      check_rotation_constraint(violations, stats, constraints)
      Ok(violations)
    }
  }
}

///|
fn constraint_status(violations : ArrayView[ConstraintViolation]) -> String {
  if violations.is_empty() {
    "pass"
  } else {
    "fail"
  }
}

///|
fn write_violation(
  out : StringBuilder,
  violation : ConstraintViolation,
) -> Unit {
  out.write_string("- [")
  out.write_string(severity_name(violation.severity))
  out.write_string("] ")
  out.write_string(violation.name)
  out.write_string(" expected=")
  out.write_string("\{violation.expected}")
  out.write_string(" actual=")
  out.write_string("\{violation.actual}")
  out.write_char('\n')
}

///|
/// Human-readable constraint check output.
pub fn to_constraint_report(
  violations : ArrayView[ConstraintViolation],
) -> String {
  let out = StringBuilder()
  out.write_string("rectloom constraint report\n")
  out.write_string("status=")
  out.write_string(constraint_status(violations))
  out.write_string(" violations=")
  out.write_string("\{violations.length()}")
  out.write_char('\n')
  for violation in violations {
    write_violation(out, violation)
  }
  out.to_string()
}

///|
/// Convenience wrapper for strict production-oriented checks.
pub fn strict_constraint_report(layout : Layout) -> Result[String, PackError] {
  match check_constraints(layout, LayoutConstraints::strict()) {
    Err(err) => Err(err)
    Ok(violations) => Ok(to_constraint_report(violations))
  }
}