///|
/// Human-readable metrics are kept as typed records so dashboards and CI can
/// consume the same result without parsing log text.
pub struct MotionReport2 {
  name : String
  metrics : Array[Double]
  budget : MotionBudget2
  quality : RenderQualityMetrics2
  passed : Bool
} derive(Debug)

///|
pub fn motion_report2(
  name : String,
  metrics : Array[Double],
  budget : MotionBudget2,
  quality : RenderQualityMetrics2,
) -> MotionReport2 {
  {
    name,
    metrics: metrics.copy(),
    budget,
    quality,
    passed: budget.accepts({
      path_length: metrics.get(0).unwrap_or_default(),
      peak_speed: metrics.get(1).unwrap_or_default(),
      peak_acceleration: metrics.get(2).unwrap_or_default(),
      average_speed: metrics.get(3).unwrap_or_default(),
      settled_time: metrics.get(4).unwrap_or_default(),
      samples: 0,
    }) &&
    quality.coverage() >= 1.0,
  }
}

///|
pub fn MotionReport2::name(self : MotionReport2) -> String {
  self.name
}

///|
pub fn MotionReport2::metrics(self : MotionReport2) -> Array[Double] {
  self.metrics.copy()
}

///|
pub fn MotionReport2::budget(self : MotionReport2) -> MotionBudget2 {
  self.budget
}

///|
pub fn MotionReport2::quality(self : MotionReport2) -> RenderQualityMetrics2 {
  self.quality
}

///|
pub fn MotionReport2::passed(self : MotionReport2) -> Bool {
  self.passed
}

///|
pub fn MotionReport2::score(self : MotionReport2) -> Double {
  let motion_score = if self.metrics.length() == 0 {
    0.0
  } else {
    1.0 /
    (1.0 + self.metrics.fold(init=0.0, fn(sum, value) { sum + value.abs() }))
  }
  (motion_score + self.quality.score()) * 0.5
}

///|
pub fn MotionReport2::merge(
  self : MotionReport2,
  other : MotionReport2,
) -> MotionReport2 {
  let metrics = self.metrics.copy()
  for value in other.metrics {
    metrics.push(value)
  }
  {
    name: self.name,
    metrics,
    budget: self.budget,
    quality: other.quality,
    passed: self.passed && other.passed,
  }
}

///|
pub fn MotionReport2::metric(self : MotionReport2, index : Int) -> Double {
  self.metrics.get(index).unwrap_or_default()
}

///|
pub fn MotionReport2::metric_count(self : MotionReport2) -> Int {
  self.metrics.length()
}

///|
pub fn MotionReport2::budget_violations(self : MotionReport2) -> Array[Double] {
  let values : MotionMetrics2 = {
    path_length: self.metric(0),
    peak_speed: self.metric(1),
    peak_acceleration: self.metric(2),
    average_speed: self.metric(3),
    settled_time: self.metric(4),
    samples: 0,
  }
  self.budget.violations(values)
}

///|
pub fn MotionReport2::quality_coverage(self : MotionReport2) -> Double {
  self.quality.coverage()
}

///|
pub fn MotionReport2::is_actionable(self : MotionReport2) -> Bool {
  self.metric_count() >= 3 && self.quality_coverage() > 0.0
}

///|
pub fn MotionReport2::has_motion_violation(self : MotionReport2) -> Bool {
  self.budget_violations().length() > 0
}

///|
pub fn MotionReport2::quality_passed(self : MotionReport2) -> Bool {
  self.quality.coverage() >= 1.0 && self.quality.dropped_frames() == 0
}

///|
pub fn MotionReport2::summary_score(self : MotionReport2) -> Double {
  if self.is_actionable() {
    self.score()
  } else {
    0.0
  }
}

///|
pub fn MotionReport2::meets_all_budgets(self : MotionReport2) -> Bool {
  !self.has_motion_violation() && self.quality_passed()
}

///|
pub fn MotionReport2::metric_sum(self : MotionReport2) -> Double {
  self.metrics.fold(init=0.0, fn(sum, value) { sum + value })
}

///|
pub fn MotionReport2::metric_average(self : MotionReport2) -> Double {
  if self.metrics.length() == 0 {
    0.0
  } else {
    self.metric_sum() / self.metrics.length().to_double()
  }
}

///|
pub fn MotionReport2::quality_error(self : MotionReport2) -> Double {
  self.quality.temporal_error() +
  self.quality.spatial_error() +
  self.quality.color_error()
}

///|
pub fn MotionReport2::needs_review(self : MotionReport2) -> Bool {
  !self.passed || self.quality_error() > 0.01
}

///|
pub fn MotionReport2::status_code(self : MotionReport2) -> Int {
  if self.meets_all_budgets() {
    0
  } else if self.needs_review() {
    2
  } else {
    1
  }
}

///|
pub fn MotionReport2::coverage_gap(self : MotionReport2) -> Double {
  (1.0 - self.quality_coverage()).max(0.0)
}

///|
pub fn MotionReport2::budget_score(self : MotionReport2) -> Double {
  let violations = self.budget_violations()
  if violations.length() == 0 {
    1.0
  } else {
    1.0 / (1.0 + violations.fold(init=0.0, fn(sum, value) { sum + value }))
  }
}

///|
pub fn MotionReport2::weighted_score(
  self : MotionReport2,
  motion_weight : Double,
) -> Double {
  let weight = clamp01(motion_weight)
  self.budget_score() * weight + self.quality.score() * (1.0 - weight)
}

///|
pub fn MotionReport2::complete(self : MotionReport2) -> Bool {
  self.status_code() == 0 && self.metric_count() > 0
}

///|
pub fn MotionReport2::error_budget(self : MotionReport2) -> Double {
  self.quality_error() +
  self.budget_violations().fold(init=0.0, fn(sum, value) { sum + value })
}

///|
pub fn MotionReport2::stable(self : MotionReport2) -> Bool {
  self.error_budget() <= 0.001
}

///|
pub fn MotionReport2::coverage_ok(self : MotionReport2) -> Bool {
  self.quality_coverage() >= 0.999
}

///|
pub fn MotionReport2::has_frames(self : MotionReport2) -> Bool {
  self.quality.dropped_frames() == 0
}

///|
pub fn MotionReport2::ready_for_export(self : MotionReport2) -> Bool {
  self.complete() && self.coverage_ok() && self.has_frames()
}

///|
pub fn MotionReport2::quality_score(self : MotionReport2) -> Double {
  self.quality.score()
}

///|
pub fn MotionReport2::export_score(self : MotionReport2) -> Double {
  self.weighted_score(0.5)
}

///|
pub fn MotionReport2::valid(self : MotionReport2) -> Bool {
  self.ready_for_export()
}