///|
/// A deterministic numerical benchmark result.
pub(all) struct BenchmarkReport {
  name : String
  cells : Int
  steps : Int
  l2_error : Double
  linf_error : Double
  mass_drift : Double
  mean_speed : Double
  stable : Bool
} derive(Debug)

///|
/// Format a benchmark result as one CSV record.
pub fn BenchmarkReport::to_csv(self : BenchmarkReport) -> String {
  "\{self.name},\{self.cells},\{self.steps},\{self.l2_error},\{self.linf_error},\{self.mass_drift},\{self.mean_speed},\{self.stable}\n"
}

///|
/// Format a benchmark result for a human-readable report.
pub fn BenchmarkReport::to_markdown(self : BenchmarkReport) -> String {
  "| \{self.name} | \{self.cells} | \{self.steps} | \{self.l2_error} | \{self.linf_error} | \{self.mass_drift} | \{self.mean_speed} | \{self.stable} |"
}

///|
/// Build the analytical channel profile used by the Poiseuille comparison.
pub fn poiseuille_reference_profile(
  height~ : Int,
  max_velocity~ : Double,
) -> Array[Double] {
  let result = Array::new()
  let denominator = (height - 1).max(1).to_double()
  for y in 0.. Double {
  if cells.length() == 0 {
    0.0
  } else {
    let mut total = 0.0
    for cell in cells {
      total += cell.speed()
    }
    total / cells.length().to_double()
  }
}

///|
/// Run a measured Poiseuille profile comparison.
pub fn run_poiseuille_benchmark(
  width~ : Int,
  height~ : Int,
  steps~ : Int,
) -> BenchmarkReport {
  let lattice = poiseuille(width=width.max(1), height=height.max(2))
  let reference_mass = lattice.mass()
  lattice.run(steps.max(0))
  let actual = lattice.horizontal_profile(y=height / 2)
  let reference = poiseuille_reference_profile(height~, max_velocity=0.0002)
  let stability = lattice.stability()
  {
    name: "poiseuille",
    cells: width.max(1) * height.max(2),
    steps: steps.max(0),
    l2_error: l2_error(reference, actual),
    linf_error: linf_error(reference, actual),
    mass_drift: abs_double(lattice.mass() - reference_mass),
    mean_speed: mean_cell_speed(lattice.cells()),
    stable: stability.recommended,
  }
}

///|
/// Run a cavity benchmark against a zero-velocity interior reference.
pub fn run_cavity_benchmark(size~ : Int, steps~ : Int) -> BenchmarkReport {
  let lattice = lid_driven_cavity(size=size.max(4))
  let reference_mass = lattice.mass()
  lattice.run(steps.max(0))
  let actual = lattice.horizontal_profile(y=size / 2)
  let reference = Array::make(size.max(4), 0.0)
  let stability = lattice.stability()
  {
    name: "lid-driven-cavity",
    cells: size.max(4) * size.max(4),
    steps: steps.max(0),
    l2_error: l2_error(reference, actual),
    linf_error: linf_error(reference, actual),
    mass_drift: abs_double(lattice.mass() - reference_mass),
    mean_speed: mean_cell_speed(lattice.cells()),
    stable: stability.recommended,
  }
}

///|
/// Run an obstacle-wake benchmark and report a vorticity magnitude summary.
pub fn run_cylinder_benchmark(
  width~ : Int,
  height~ : Int,
  steps~ : Int,
) -> BenchmarkReport {
  let lattice = cylinder_wake(
    width=width.max(8),
    height=height.max(6),
    radius=2,
  )
  let reference_mass = lattice.mass()
  lattice.run(steps.max(0))
  let actual = lattice.horizontal_profile(y=height / 2)
  let reference = Array::make(width.max(8), 0.0)
  let stability = lattice.stability()
  {
    name: "cylinder-wake",
    cells: width.max(8) * height.max(6),
    steps: steps.max(0),
    l2_error: l2_error(reference, actual),
    linf_error: linf_error(reference, actual),
    mass_drift: abs_double(lattice.mass() - reference_mass),
    mean_speed: mean_cell_speed(lattice.cells()),
    stable: stability.recommended,
  }
}

///|
/// Run the three reference cases used by the CLI and documentation.
pub fn run_benchmark_suite() -> Array[BenchmarkReport] {
  [
    run_poiseuille_benchmark(width=24, height=10, steps=30),
    run_cavity_benchmark(size=20, steps=20),
    run_cylinder_benchmark(width=32, height=16, steps=20),
  ]
}

///|
/// Export a suite as CSV with a stable header.
pub fn benchmark_suite_csv(reports : ArrayView[BenchmarkReport]) -> String {
  let builder = StringBuilder(size_hint=128 + reports.length() * 96)
  builder.write_string(
    "name,cells,steps,l2_error,linf_error,mass_drift,mean_speed,stable\n",
  )
  for report in reports {
    builder.write_string(report.to_csv())
  }
  builder.to_string()
}

///|
/// Export a suite as a Markdown table.
pub fn benchmark_suite_markdown(reports : ArrayView[BenchmarkReport]) -> String {
  let builder = StringBuilder(size_hint=256 + reports.length() * 128)
  builder.write_string(
    "| case | cells | steps | L2 error | Linf error | mass drift | mean speed | stable |\n",
  )
  builder.write_string("|---|---:|---:|---:|---:|---:|---:|:---:|\n")
  for report in reports {
    builder.write_string(report.to_markdown())
    builder.write_char('\n')
  }
  builder.to_string()
}

///|
/// Return the absolute value without relying on a target-specific helper.
pub fn abs_double(value : Double) -> Double {
  if value < 0.0 {
    -value
  } else {
    value
  }
}

///|
/// Return the number of stable reports in a suite.
pub fn stable_benchmark_count(reports : ArrayView[BenchmarkReport]) -> Int {
  let mut count = 0
  for report in reports {
    if report.stable {
      count += 1
    }
  }
  count
}