///|
/// Structured diagnostics for tooling and CI. Reports contain measurements
/// rather than formatted terminal output so host applications can render
/// warnings in their own UI.
pub enum DiagnosticSeverity2 {
  InfoDiagnostic
  WarningDiagnostic
  ErrorDiagnostic
} derive(Debug)

///|
pub fn diagnostic_severities2() -> Array[DiagnosticSeverity2] {
  [InfoDiagnostic, WarningDiagnostic, ErrorDiagnostic]
}

///|
pub fn diagnostic_severity_name2(severity : DiagnosticSeverity2) -> String {
  match severity {
    InfoDiagnostic => "info"
    WarningDiagnostic => "warning"
    ErrorDiagnostic => "error"
  }
}

///|
pub enum DiagnosticCode2 {
  EmptyInputDiagnostic
  NonFiniteDiagnostic
  EndpointMismatchDiagnostic
  VelocityLimitDiagnostic
  SamplingErrorDiagnostic
  ColorRangeDiagnostic
  BoundsDiagnostic
} derive(Debug)

///|
pub fn diagnostic_codes2() -> Array[DiagnosticCode2] {
  [
    EmptyInputDiagnostic,
    NonFiniteDiagnostic,
    EndpointMismatchDiagnostic,
    VelocityLimitDiagnostic,
    SamplingErrorDiagnostic,
    ColorRangeDiagnostic,
    BoundsDiagnostic,
  ]
}

///|
pub fn diagnostic_code_name2(code : DiagnosticCode2) -> String {
  match code {
    EmptyInputDiagnostic => "empty-input"
    NonFiniteDiagnostic => "non-finite"
    EndpointMismatchDiagnostic => "endpoint-mismatch"
    VelocityLimitDiagnostic => "velocity-limit"
    SamplingErrorDiagnostic => "sampling-error"
    ColorRangeDiagnostic => "color-range"
    BoundsDiagnostic => "bounds"
  }
}

///|
pub struct DiagnosticIssue2 {
  severity : DiagnosticSeverity2
  code : DiagnosticCode2
  message : String
  sample_index : Int
  value : Double
  limit : Double
} derive(Debug)

///|
pub fn DiagnosticIssue2::severity(
  self : DiagnosticIssue2,
) -> DiagnosticSeverity2 {
  self.severity
}

///|
pub fn DiagnosticIssue2::code(self : DiagnosticIssue2) -> DiagnosticCode2 {
  self.code
}

///|
pub fn DiagnosticIssue2::message(self : DiagnosticIssue2) -> String {
  self.message
}

///|
pub fn DiagnosticIssue2::sample_index(self : DiagnosticIssue2) -> Int {
  self.sample_index
}

///|
pub fn DiagnosticIssue2::value(self : DiagnosticIssue2) -> Double {
  self.value
}

///|
pub fn DiagnosticIssue2::limit(self : DiagnosticIssue2) -> Double {
  self.limit
}

///|
pub struct DiagnosticReport2 {
  name : String
  issues : Array[DiagnosticIssue2]
  checked_samples : Int
  duration : Double
} derive(Debug)

///|
pub fn diagnostic_report2(name : String) -> DiagnosticReport2 {
  { name, issues: [], checked_samples: 0, duration: 0.0 }
}

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

///|
pub fn DiagnosticReport2::issues(
  self : DiagnosticReport2,
) -> Array[DiagnosticIssue2] {
  self.issues.copy()
}

///|
pub fn DiagnosticReport2::checked_samples(self : DiagnosticReport2) -> Int {
  self.checked_samples
}

///|
pub fn DiagnosticReport2::duration(self : DiagnosticReport2) -> Double {
  self.duration
}

///|
pub fn DiagnosticReport2::error_count(self : DiagnosticReport2) -> Int {
  let mut count = 0
  for issue in self.issues {
    if issue.severity is ErrorDiagnostic {
      count = count + 1
    }
  }
  count
}

///|
pub fn DiagnosticReport2::warning_count(self : DiagnosticReport2) -> Int {
  let mut count = 0
  for issue in self.issues {
    if issue.severity is WarningDiagnostic {
      count = count + 1
    }
  }
  count
}

///|
pub fn DiagnosticReport2::add(
  self : DiagnosticReport2,
  issue : DiagnosticIssue2,
) -> Unit {
  self.issues.push(issue)
}

///|
pub fn DiagnosticReport2::with_measurements(
  self : DiagnosticReport2,
  samples : Int,
  duration : Double,
) -> DiagnosticReport2 {
  {
    name: self.name,
    issues: self.issues,
    checked_samples: samples.max(0),
    duration: duration.max(0.0),
  }
}

///|
fn diagnostic_issue(
  severity : DiagnosticSeverity2,
  code : DiagnosticCode2,
  message : String,
  index : Int,
  value : Double,
  limit : Double,
) -> DiagnosticIssue2 {
  { severity, code, message, sample_index: index, value, limit }
}

///|
pub fn inspect_scalar_values(
  name : String,
  values : Array[Double],
  tolerance : Double,
) -> DiagnosticReport2 {
  let report = diagnostic_report2(name).with_measurements(values.length(), 0.0)
  if values.length() == 0 {
    report.add(
      diagnostic_issue(
        WarningDiagnostic,
        EmptyInputDiagnostic,
        "no samples were provided",
        0,
        0.0,
        0.0,
      ),
    )
    return report
  }
  for index in 0.. 0 &&
      (value - values[index - 1]).abs() > tolerance.abs() * 1000000.0 {
      report.add(
        diagnostic_issue(
          WarningDiagnostic,
          SamplingErrorDiagnostic,
          "adjacent samples change abruptly",
          index,
          (value - values[index - 1]).abs(),
          tolerance,
        ),
      )
    }
  }
  report
}

///|
pub fn inspect_curve_endpoints(
  name : String,
  curve : Curve,
  tolerance : Double,
) -> DiagnosticReport2 {
  let report = diagnostic_report2(name).with_measurements(2, 1.0)
  let start = curve.apply(0.0)
  let end = curve.apply(1.0)
  if start.abs() > tolerance.abs() {
    report.add(
      diagnostic_issue(
        WarningDiagnostic,
        EndpointMismatchDiagnostic,
        "curve does not start at zero",
        0,
        start,
        tolerance,
      ),
    )
  }
  if (end - 1.0).abs() > tolerance.abs() {
    report.add(
      diagnostic_issue(
        WarningDiagnostic,
        EndpointMismatchDiagnostic,
        "curve does not end at one",
        1,
        end,
        1.0,
      ),
    )
  }
  report
}

///|
pub fn inspect_motion_limits(
  name : String,
  samples : Array[Double],
  frame_rate : Double,
  maximum_velocity : Double,
) -> DiagnosticReport2 {
  let report = diagnostic_report2(name).with_measurements(samples.length(), 0.0)
  if samples.length() < 2 {
    return report
  }
  let dt = 1.0 / frame_rate.max(0.001)
  for index in 1.. maximum_velocity {
      report.add(
        diagnostic_issue(
          WarningDiagnostic,
          VelocityLimitDiagnostic,
          "sample exceeds velocity budget",
          index,
          velocity,
          maximum_velocity,
        ),
      )
    }
  }
  report
}

///|
pub fn inspect_colors(name : String, colors : Array[Rgba]) -> DiagnosticReport2 {
  let report = diagnostic_report2(name).with_measurements(colors.length(), 0.0)
  for index in 0.. 1.0 ||
      color.green() < 0.0 ||
      color.green() > 1.0 ||
      color.blue() < 0.0 ||
      color.blue() > 1.0 ||
      color.alpha() < 0.0 ||
      color.alpha() > 1.0 {
      report.add(
        diagnostic_issue(
          ErrorDiagnostic,
          ColorRangeDiagnostic,
          "color channel is outside normalized range",
          index,
          color.alpha(),
          1.0,
        ),
      )
    }
  }
  report
}

///|
pub struct DiagnosticSummary2 {
  reports : Int
  errors : Int
  warnings : Int
  samples : Int
} derive(Debug)

///|
pub fn summarize_diagnostics(
  reports : Array[DiagnosticReport2],
) -> DiagnosticSummary2 {
  let mut errors = 0
  let mut warnings = 0
  let mut samples = 0
  for report in reports {
    errors = errors + report.error_count()
    warnings = warnings + report.warning_count()
    samples = samples + report.checked_samples
  }
  { reports: reports.length(), errors, warnings, samples }
}

///|
pub fn DiagnosticSummary2::reports(self : DiagnosticSummary2) -> Int {
  self.reports
}

///|
pub fn DiagnosticSummary2::errors(self : DiagnosticSummary2) -> Int {
  self.errors
}

///|
pub fn DiagnosticSummary2::warnings(self : DiagnosticSummary2) -> Int {
  self.warnings
}

///|
pub fn DiagnosticSummary2::samples(self : DiagnosticSummary2) -> Int {
  self.samples
}

///|
pub fn DiagnosticSummary2::passed(self : DiagnosticSummary2) -> Bool {
  self.errors == 0
}