///|
pub(all) struct QualityMetric {
  name : String
  value : Double
  tolerance : Double
  passed : Bool
} derive(Debug, ToJson)

///|
pub fn metric(
  name : String,
  value : Double,
  tolerance : Double,
) -> QualityMetric {
  { name, value, tolerance, passed: value <= tolerance }
}

///|
pub fn metric_l2(
  name : String,
  actual : ArrayView[Double],
  expected : ArrayView[Double],
  tolerance : Double,
) -> QualityMetric {
  metric(name, field_l2_error(actual, expected), tolerance)
}

///|
pub fn metric_max_abs(
  name : String,
  values : ArrayView[Double],
  tolerance : Double,
) -> QualityMetric {
  metric(name, max_abs_value(values), tolerance)
}

///|
pub fn metric_relative(
  name : String,
  actual : Double,
  expected : Double,
  tolerance : Double,
) -> QualityMetric {
  metric(name, relative_error(expected, actual), tolerance)
}

///|
pub fn metric_count(
  name : String,
  actual : Int,
  expected : Int,
) -> QualityMetric {
  metric(name, (actual - expected).abs().to_double(), 0.0)
}

///|
pub fn all_metrics_pass(metrics : ArrayView[QualityMetric]) -> Bool {
  metrics.all(fn(item) { item.passed })
}

///|
pub fn maximum_metric(metrics : ArrayView[QualityMetric]) -> Double {
  max_value(metrics.map(fn(item) { item.value }), 0.0)
}

///|
pub fn metric_names(metrics : ArrayView[QualityMetric]) -> Array[String] {
  metrics.map(fn(item) { item.name })
}

///|
pub fn metrics_to_csv(metrics : ArrayView[QualityMetric]) -> String {
  let output = StringBuilder()
  output.write_string("name,value,tolerance,passed\n")
  for item in metrics {
    output.write_string(
      "\{item.name},\{item.value},\{item.tolerance},\{item.passed}\n",
    )
  }
  output.to_string()
}

///|
pub fn field_quality_metrics(
  grid : Grid1D,
  field : Field1D,
) -> Array[QualityMetric] {
  [
    metric_count("electric_length", field.electric.length(), grid.cells),
    metric_count("potential_length", field.potential.length(), grid.cells),
    metric("electric_mean", mean(field.electric).abs(), 1.0e-8),
    metric("potential_mean", mean(field.potential).abs(), 1.0e-8),
  ]
}

///|
pub fn particle_quality_metrics(
  grid : Grid1D,
  particles : ArrayView[Particle],
) -> Array[QualityMetric] {
  [
    metric(
      "position_range",
      if boundary_valid(grid, particles) {
        0.0
      } else {
        1.0
      },
      0.0,
    ),
    metric("weight_sum", total_particle_charge(particles).abs(), 1.0e100),
    metric("speed_range", sample_span(particle_speeds(particles)), 1.0e100),
  ]
}

///|
pub fn scenario_quality_metrics(
  scenario : SimulationScenario,
) -> Array[QualityMetric] {
  let state = scenario_run(scenario)
  let metrics = particle_quality_metrics(scenario.grid, state.particles)
  let field_metrics = field_quality_metrics(scenario.grid, state.field)
  let output = metrics.copy()
  for item in field_metrics {
    output.push(item)
  }
  output
}

///|
pub fn quality_report(metrics : ArrayView[QualityMetric]) -> String {
  let output = StringBuilder()
  output.write_string("passed=\{all_metrics_pass(metrics)}\n")
  output.write_string(metrics_to_csv(metrics))
  output.to_string()
}

///|
pub fn compare_metric_sets(
  first : ArrayView[QualityMetric],
  second : ArrayView[QualityMetric],
) -> Double {
  let count = first.length().min(second.length())
  let mut total = 0.0
  for i in 0..