///|
pub fn severity_name(severity : IssueSeverity) -> String {
  match severity {
    Info => "info"
    Warning => "warning"
    Critical => "critical"
  }
}

///|
pub fn issue_kind_name(kind : IssueKind) -> String {
  match kind {
    LowOccupancy => "low-occupancy"
    HasUnpacked => "has-unpacked"
    WideBoundingBox => "wide-bounding-box"
    TallBoundingBox => "tall-bounding-box"
    SparseGrid => "sparse-grid"
    HeavyRotation => "heavy-rotation"
    FragmentedRows => "fragmented-rows"
    ConstraintFailed => "constraint-failed"
    SourceRisk => "source-risk"
  }
}

///|
fn make_issue(
  severity : IssueSeverity,
  kind : IssueKind,
  id : String,
  message : String,
  value : Int,
) -> LayoutIssue {
  LayoutIssue::{ severity, kind, id, message, value }
}

///|
fn push_issue(
  issues : Array[LayoutIssue],
  severity : IssueSeverity,
  kind : IssueKind,
  id : String,
  message : String,
  value : Int,
) -> Unit {
  issues.push(make_issue(severity, kind, id, message, value))
}

///|
fn diagnose_low_occupancy(
  issues : Array[LayoutIssue],
  stats : LayoutStats,
) -> Unit {
  if stats.placed_count == 0 {
    push_issue(
      issues,
      Critical,
      LowOccupancy,
      "layout",
      "no rectangle was placed",
      stats.occupancy_permille,
    )
  } else if stats.occupancy_permille < 350 {
    push_issue(
      issues,
      Warning,
      LowOccupancy,
      "layout",
      "container occupancy is low",
      stats.occupancy_permille,
    )
  } else if stats.occupancy_permille >= 750 {
    push_issue(
      issues,
      Info,
      LowOccupancy,
      "layout",
      "container occupancy is strong",
      stats.occupancy_permille,
    )
  }
}

///|
fn diagnose_unpacked(issues : Array[LayoutIssue], stats : LayoutStats) -> Unit {
  if stats.unpacked_count > 0 {
    push_issue(
      issues,
      Critical,
      HasUnpacked,
      "layout",
      "some rectangles were not packed",
      stats.unpacked_count,
    )
  }
}

///|
fn diagnose_bounding_box(
  issues : Array[LayoutIssue],
  layout : Layout,
  stats : LayoutStats,
) -> Unit {
  let width_ratio = scale_permille(stats.bounding_width, layout.bin.width)
  let height_ratio = scale_permille(stats.bounding_height, layout.bin.height)
  if width_ratio > 900 && stats.occupancy_permille < 600 {
    push_issue(
      issues,
      Warning,
      WideBoundingBox,
      "layout",
      "placements stretch across most of the width while occupancy is modest",
      width_ratio,
    )
  }
  if height_ratio > 900 && stats.occupancy_permille < 600 {
    push_issue(
      issues,
      Warning,
      TallBoundingBox,
      "layout",
      "placements stretch across most of the height while occupancy is modest",
      height_ratio,
    )
  }
}

///|
fn diagnose_fragmentation(
  issues : Array[LayoutIssue],
  stats : LayoutStats,
) -> Unit {
  if stats.fragmentation_score > 650 {
    push_issue(
      issues,
      Warning,
      FragmentedRows,
      "layout",
      "row and column anchors look fragmented",
      stats.fragmentation_score,
    )
  } else if stats.fragmentation_score < 150 && stats.placed_count > 2 {
    push_issue(
      issues,
      Info,
      FragmentedRows,
      "layout",
      "row and column anchors are compact",
      stats.fragmentation_score,
    )
  }
}

///|
fn diagnose_rotation(issues : Array[LayoutIssue], stats : LayoutStats) -> Unit {
  if stats.placed_count == 0 {
    return
  }
  let ratio = scale_permille(stats.rotated_count, stats.placed_count)
  if ratio > 700 {
    push_issue(
      issues,
      Warning,
      HeavyRotation,
      "layout",
      "most placements require rotation",
      ratio,
    )
  } else if ratio > 0 {
    push_issue(
      issues,
      Info,
      HeavyRotation,
      "layout",
      "some placements use rotation",
      ratio,
    )
  }
}

///|
fn diagnose_grid_sparsity(issues : Array[LayoutIssue], layout : Layout) -> Unit {
  let cell_w = (layout.bin.width / 4).max(1)
  let cell_h = (layout.bin.height / 4).max(1)
  match density_grid(layout, cell_width=cell_w, cell_height=cell_h) {
    Err(_) => ()
    Ok(report) => {
      let empty_ratio = scale_permille(
        report.empty_cells,
        report.cells.length(),
      )
      if empty_ratio > 700 && !layout.placements.is_empty() {
        push_issue(
          issues,
          Warning,
          SparseGrid,
          "layout",
          "coarse grid has many empty cells",
          empty_ratio,
        )
      }
    }
  }
}

///|
/// Diagnose a completed layout and return structured findings.
pub fn diagnose_layout(
  layout : Layout,
) -> Result[Array[LayoutIssue], PackError] {
  match analyze_layout(layout) {
    Err(err) => Err(err)
    Ok(stats) => {
      let issues : Array[LayoutIssue] = []
      diagnose_low_occupancy(issues, stats)
      diagnose_unpacked(issues, stats)
      diagnose_bounding_box(issues, layout, stats)
      diagnose_fragmentation(issues, stats)
      diagnose_rotation(issues, stats)
      diagnose_grid_sparsity(issues, layout)
      Ok(issues)
    }
  }
}

///|
fn diagnose_invalid_rects(
  issues : Array[LayoutIssue],
  rects : ArrayView[Rect],
) -> Unit {
  for rect in rects {
    if rect.id.is_empty() {
      push_issue(
        issues,
        Critical,
        SourceRisk,
        "rect",
        "rectangle id is empty",
        0,
      )
    }
    if rect.width <= 0 || rect.height <= 0 {
      push_issue(
        issues,
        Critical,
        SourceRisk,
        rect.id,
        "rectangle size must be positive",
        rect.width * rect.height,
      )
    }
  }
}

///|
fn diagnose_duplicate_rects(
  issues : Array[LayoutIssue],
  rects : ArrayView[Rect],
) -> Unit {
  match maybe_duplicate_id(rects) {
    Some(id) =>
      push_issue(issues, Critical, SourceRisk, id, "duplicate rectangle id", 1)
    None => ()
  }
}

///|
fn diagnose_rect_shape_mix(
  issues : Array[LayoutIssue],
  stats : RectSetStats,
) -> Unit {
  if stats.count == 0 {
    push_issue(
      issues,
      Critical,
      SourceRisk,
      "rects",
      "input rectangle collection is empty",
      0,
    )
    return
  }
  if stats.max_area > stats.min_area * 20 && stats.min_area > 0 {
    push_issue(
      issues,
      Warning,
      SourceRisk,
      "rects",
      "rectangle areas vary widely",
      scale_permille(stats.max_area, stats.min_area),
    )
  }
  if stats.oversized_count > 0 {
    push_issue(
      issues,
      Critical,
      SourceRisk,
      "rects",
      "some rectangles are larger than the target bin",
      stats.oversized_count,
    )
  }
  if stats.wide_count == stats.count || stats.tall_count == stats.count {
    push_issue(
      issues,
      Info,
      SourceRisk,
      "rects",
      "all rectangles share the same broad orientation",
      stats.count,
    )
  }
}

///|
/// Diagnose source rectangles against a target bin.
pub fn diagnose_rects_for_bin(
  rects : ArrayView[Rect],
  bin : Bin,
) -> Array[LayoutIssue] {
  let issues : Array[LayoutIssue] = []
  diagnose_invalid_rects(issues, rects)
  diagnose_duplicate_rects(issues, rects)
  diagnose_rect_shape_mix(issues, summarize_rects_for_bin(rects, bin))
  issues
}

///|
/// Diagnose source rectangles without a target bin.
pub fn diagnose_rects(rects : ArrayView[Rect]) -> Array[LayoutIssue] {
  let issues : Array[LayoutIssue] = []
  diagnose_invalid_rects(issues, rects)
  diagnose_duplicate_rects(issues, rects)
  diagnose_rect_shape_mix(issues, summarize_rects(rects))
  issues
}

///|
fn count_severity(
  issues : ArrayView[LayoutIssue],
  severity : IssueSeverity,
) -> Int {
  let mut count = 0
  for issue in issues {
    if issue.severity == severity {
      count = count + 1
    }
  }
  count
}

///|
fn write_issue(out : StringBuilder, issue : LayoutIssue) -> Unit {
  out.write_string("- [")
  out.write_string(severity_name(issue.severity))
  out.write_string("] ")
  out.write_string(issue_kind_name(issue.kind))
  out.write_string(" ")
  out.write_string(issue.id)
  out.write_string(": ")
  out.write_string(issue.message)
  out.write_string(" (")
  out.write_string("\{issue.value}")
  out.write_string(")")
  out.write_char('\n')
}

///|
/// Human-readable diagnostic report.
pub fn to_diagnostics_report(issues : ArrayView[LayoutIssue]) -> String {
  let out = StringBuilder()
  out.write_string("rectloom diagnostics\n")
  out.write_string("critical=")
  out.write_string("\{count_severity(issues, Critical)}")
  out.write_string(" warning=")
  out.write_string("\{count_severity(issues, Warning)}")
  out.write_string(" info=")
  out.write_string("\{count_severity(issues, Info)}")
  out.write_char('\n')
  for issue in issues {
    write_issue(out, issue)
  }
  out.to_string()
}

///|
/// Convenience wrapper for layout diagnostics.
pub fn diagnose_layout_report(layout : Layout) -> Result[String, PackError] {
  match diagnose_layout(layout) {
    Err(err) => Err(err)
    Ok(issues) => Ok(to_diagnostics_report(issues))
  }
}