///|
/// Detailed diagnostics for a residual consensus set.
pub(all) struct DiagnosticReport {
  total : Int
  inliers : Int
  outliers : Int
  inlier_ratio : Double
  mean_inlier : Double
  rms : Double
  maximum : Double
  label : String
} derive(Debug, Eq)

///|
pub fn make_diagnostic_report(
  residuals : ArrayView[Double],
  threshold~ : Double,
) -> DiagnosticReport {
  let mut inliers = 0
  let mut sum = 0.0
  let mut square = 0.0
  let mut maximum = 0.0
  for residual in residuals {
    let value = residual.abs()
    square += value * value
    maximum = maximum.max(value)
    if value <= threshold {
      inliers += 1
      sum += value
    }
  }
  let ratio = if residuals.length() == 0 {
    0.0
  } else {
    inliers.to_double() / residuals.length().to_double()
  }
  let mean = if inliers == 0 { 0.0 } else { sum / inliers.to_double() }
  let rms = if residuals.length() == 0 {
    0.0
  } else {
    (square / residuals.length().to_double()).sqrt()
  }
  {
    total: residuals.length(),
    inliers,
    outliers: residuals.length() - inliers,
    inlier_ratio: ratio,
    mean_inlier: mean,
    rms,
    maximum,
    label: diagnostic_label(ratio, mean, threshold),
  }
}

///|
pub fn diagnostic_label(
  inlier_ratio : Double,
  mean_inlier : Double,
  threshold : Double,
) -> String {
  if inlier_ratio >= 0.75 && mean_inlier <= threshold * 0.5 {
    "good"
  } else if inlier_ratio >= 0.5 {
    "review"
  } else {
    "poor"
  }
}

///|
pub fn diagnostic_report_is_good(
  report : DiagnosticReport,
  minimum_ratio : Double,
  maximum_rms : Double,
) -> Bool {
  report.inlier_ratio >= minimum_ratio &&
  report.rms <= maximum_rms &&
  report.inliers > 0
}

///|
pub fn diagnostic_outlier_indices(
  residuals : ArrayView[Double],
  threshold : Double,
) -> Array[Int] {
  let result : Array[Int] = []
  for i in 0.. threshold {
      result.push(i)
    }
  }
  result
}

///|
pub fn diagnostic_inlier_indices(
  residuals : ArrayView[Double],
  threshold : Double,
) -> Array[Int] {
  let result : Array[Int] = []
  for i in 0.. Array[Int] {
  let result : Array[Int] = []
  for _ in 0.. 0 {
      let index = (residual.abs() / width * result.length().to_double())
        .to_int()
        .min(result.length() - 1)
        .max(0)
      result[index] += 1
    }
  }
  result
}

///|
pub fn diagnostic_percentile(
  residuals : ArrayView[Double],
  probability : Double,
) -> Double raise @core.GeometryError {
  let values : Array[Double] = []
  for residual in residuals {
    values.push(residual.abs())
  }
  @core.quantile_linear(values, probability)
}

///|
pub fn diagnostic_mad(
  residuals : ArrayView[Double],
) -> Double raise @core.GeometryError {
  let values : Array[Double] = []
  for residual in residuals {
    values.push(residual.abs())
  }
  @core.median_absolute_deviation2(values)
}

///|
pub fn diagnostic_adaptive_threshold(
  residuals : ArrayView[Double],
  multiplier : Double,
  minimum : Double,
) -> Double raise @core.GeometryError {
  (diagnostic_mad(residuals) * multiplier).max(minimum)
}

///|
pub fn diagnostic_score(report : DiagnosticReport, scale : Double) -> Double {
  let denominator = scale.abs().max(0.000000000001)
  report.inlier_ratio * (1.0 - report.mean_inlier / denominator).max(0.0)
}

///|
pub fn diagnostic_compare(
  left : DiagnosticReport,
  right : DiagnosticReport,
) -> Int {
  if left.inliers != right.inliers {
    if left.inliers > right.inliers {
      -1
    } else {
      1
    }
  } else if left.rms < right.rms {
    -1
  } else if left.rms > right.rms {
    1
  } else {
    0
  }
}

///|
pub fn diagnostic_rank(
  reports : ArrayView[DiagnosticReport],
) -> Array[DiagnosticReport] {
  let result = reports.to_owned()
  result.sort_by(fn(left, right) { diagnostic_compare(left, right) })
  result
}