///|
/// Batch calibration diagnostics for repeated image measurements.
pub(all) struct CalibrationBatchSummary {
  count : Int
  mean_error : Double
  maximum_error : Double
  median_error : Double
  valid_ratio : Double
  score : Double
} derive(Debug, Eq)

///|
pub fn calibration_error_stats(
  errors : ArrayView[Double],
) -> CalibrationBatchSummary {
  let mut valid = 0
  let mut total = 0.0
  let mut maximum = 0.0
  for error in errors {
    if !error.is_nan() && error >= 0.0 {
      valid += 1
      total += error
      maximum = maximum.max(error)
    }
  }
  let valid_values : Array[Double] = []
  for error in errors {
    if !error.is_nan() && error >= 0.0 {
      valid_values.push(error)
    }
  }
  let mean_error = if valid == 0 { 0.0 } else { total / valid.to_double() }
  let median_error = @core.median(valid_values) catch { _ => 0.0 }
  let valid_ratio = if errors.length() == 0 {
    0.0
  } else {
    valid.to_double() / errors.length().to_double()
  }
  {
    count: errors.length(),
    mean_error,
    maximum_error: maximum,
    median_error,
    valid_ratio,
    score: valid_ratio / (1.0 + mean_error),
  }
}

///|
pub fn calibration_errors_from_pairs(
  predicted : ArrayView[@core.Point2],
  observed : ArrayView[@core.Point2],
) -> Array[Double] {
  let result : Array[Double] = []
  let count = predicted.length().min(observed.length())
  for i in 0.. Array[Double] {
  let result : Array[Double] = []
  let count = predicted.length().min(observed.length())
  for i in 0.. Double {
  if errors.length() == 0 {
    0.0
  } else {
    let mut total = 0.0
    for error in errors {
      total += error * error
    }
    (total / errors.length().to_double()).sqrt()
  }
}

///|
pub fn calibration_mean_error(errors : ArrayView[Double]) -> Double {
  @core.mean(errors) catch {
    _ => 0.0
  }
}

///|
pub fn calibration_max_error(errors : ArrayView[Double]) -> Double {
  let mut result = 0.0
  for error in errors {
    result = result.max(error.abs())
  }
  result
}

///|
pub fn calibration_percentile_error(
  errors : ArrayView[Double],
  percentile : Double,
) -> Double {
  @core.quantile_linear(errors, percentile) catch {
    _ => 0.0
  }
}

///|
pub fn calibration_inlier_flags(
  errors : ArrayView[Double],
  threshold : Double,
) -> Array[Bool] {
  let result : Array[Bool] = []
  for error in errors {
    result.push(!error.is_nan() && error <= threshold.max(0.0))
  }
  result
}

///|
pub fn calibration_batch_inlier_count(
  errors : ArrayView[Double],
  threshold : Double,
) -> Int {
  let mut result = 0
  for error in errors {
    if !error.is_nan() && error <= threshold.max(0.0) {
      result += 1
    }
  }
  result
}

///|
pub fn calibration_batch_inlier_ratio(
  errors : ArrayView[Double],
  threshold : Double,
) -> Double {
  if errors.length() == 0 {
    0.0
  } else {
    calibration_batch_inlier_count(errors, threshold).to_double() /
    errors.length().to_double()
  }
}

///|
pub fn calibration_trimmed_errors(
  errors : ArrayView[Double],
  trim_ratio : Double,
) -> Array[Double] {
  let sorted = errors.to_owned()
  sorted.sort()
  let remove = (sorted.length().to_double() * trim_ratio.max(0.0).min(0.49)).to_int()
  let result : Array[Double] = []
  let end = (sorted.length() - remove).max(remove)
  for i in remove.. Double {
  @core.mean(calibration_trimmed_errors(errors, trim_ratio)) catch {
    _ => 0.0
  }
}

///|
pub fn calibration_weighted_error(
  errors : ArrayView[Double],
  weights : ArrayView[Double],
) -> Double {
  let count = errors.length().min(weights.length())
  let mut total = 0.0
  let mut scale = 0.0
  for i in 0.. Array[Double] {
  let result : Array[Double] = []
  for threshold in thresholds {
    result.push(calibration_batch_inlier_ratio(errors, threshold))
  }
  result
}

///|
pub fn calibration_auc(
  thresholds : ArrayView[Double],
  values : ArrayView[Double],
) -> Double {
  let count = thresholds.length().min(values.length())
  if count < 2 {
    0.0
  } else {
    let mut area = 0.0
    for i in 0..<(count - 1) {
      area += (thresholds[i + 1] - thresholds[i]).abs() *
        (values[i + 1] + values[i]) *
        0.5
    }
    area
  }
}

///|
pub fn calibration_error_histogram(
  errors : ArrayView[Double],
  bins : Int,
  maximum : Double,
) -> Array[Int] {
  let result = Array::make(bins.max(0), 0)
  if maximum <= 0.0 {
    return result
  }
  for error in errors {
    let index = (error.max(0.0) / maximum * bins.to_double())
      .to_int()
      .min(result.length() - 1)
      .max(0)
    if result.length() > 0 {
      result[index] += 1
    }
  }
  result
}

///|
pub fn calibration_histogram_peak(histogram : ArrayView[Int]) -> Int {
  let mut result = 0
  for i in 0.. histogram[result] {
      result = i
    }
  }
  result
}

///|
pub fn calibration_histogram_mass(
  histogram : ArrayView[Int],
  first : Int,
  last : Int,
) -> Int {
  let mut result = 0
  let start = first.max(0)
  let end = last.min(histogram.length()).max(start)
  for i in start.. String {
  if summary.valid_ratio < 0.5 {
    "invalid"
  } else if summary.score >= 0.8 {
    "excellent"
  } else if summary.score >= 0.5 {
    "usable"
  } else {
    "review"
  }
}

///|
pub fn calibration_report_csv(summary : CalibrationBatchSummary) -> String {
  summary.count.to_string() +
  "," +
  summary.mean_error.to_string() +
  "," +
  summary.maximum_error.to_string() +
  "," +
  summary.median_error.to_string() +
  "," +
  summary.valid_ratio.to_string() +
  "," +
  summary.score.to_string()
}

///|
pub fn calibration_batch_merge(
  left : CalibrationBatchSummary,
  right : CalibrationBatchSummary,
) -> CalibrationBatchSummary {
  let count = left.count + right.count
  let mean = if count == 0 {
    0.0
  } else {
    (
      left.mean_error * left.count.to_double() +
      right.mean_error * right.count.to_double()
    ) /
    count.to_double()
  }
  {
    count,
    mean_error: mean,
    maximum_error: left.maximum_error.max(right.maximum_error),
    median_error: (left.median_error + right.median_error) * 0.5,
    valid_ratio: (left.valid_ratio + right.valid_ratio) * 0.5,
    score: (left.score + right.score) * 0.5,
  }
}

///|
pub fn calibration_batch_merge_all(
  summaries : ArrayView[CalibrationBatchSummary],
) -> CalibrationBatchSummary {
  if summaries.length() == 0 {
    calibration_error_stats([])
  } else {
    let mut result = summaries[0]
    for summary in summaries[1:] {
      result = calibration_batch_merge(result, summary)
    }
    result
  }
}

///|
pub fn calibration_error_improvement(
  previous : ArrayView[Double],
  current : ArrayView[Double],
) -> Double {
  let a = calibration_rms_error(previous)
  let b = calibration_rms_error(current)
  if a <= 0.0 {
    0.0
  } else {
    (a - b) / a
  }
}

///|
pub fn calibration_is_improving(
  history : ArrayView[Double],
  tolerance : Double,
) -> Bool {
  if history.length() < 2 {
    true
  } else {
    history[history.length() - 1] <=
    history[history.length() - 2] + tolerance.max(0.0)
  }
}

///|
pub fn calibration_stop(
  history : ArrayView[Double],
  patience : Int,
  tolerance : Double,
) -> Bool {
  if patience <= 0 || history.length() <= patience {
    false
  } else {
    let reference = history[history.length() - patience - 1]
    let mut best = reference
    for value in history[history.length() - patience:] {
      best = best.min(value)
    }
    reference - best <= tolerance.max(0.0)
  }
}

///|
pub fn calibration_parameter_delta(
  before : CameraIntrinsics,
  after : CameraIntrinsics,
) -> Double {
  let values = [
    after.fx - before.fx,
    after.fy - before.fy,
    after.cx - before.cx,
    after.cy - before.cy,
    after.skew - before.skew,
  ]
  let mut total = 0.0
  for value in values {
    total += value * value
  }
  total.sqrt()
}

///|
pub fn calibration_intrinsics_interpolate(
  before : CameraIntrinsics,
  after : CameraIntrinsics,
  amount : Double,
) -> CameraIntrinsics {
  let t = amount.max(0.0).min(1.0)
  CameraIntrinsics::new(
    fx=before.fx + (after.fx - before.fx) * t,
    fy=before.fy + (after.fy - before.fy) * t,
    cx=before.cx + (after.cx - before.cx) * t,
    cy=before.cy + (after.cy - before.cy) * t,
    skew=before.skew + (after.skew - before.skew) * t,
  )
}

///|
pub fn calibration_intrinsics_smooth(
  history : ArrayView[CameraIntrinsics],
) -> CameraIntrinsics {
  if history.length() == 0 {
    CameraIntrinsics::new(fx=1.0, fy=1.0, cx=0.0, cy=0.0, skew=0.0)
  } else {
    let mut result = history[0]
    for value in history[1:] {
      result = calibration_intrinsics_interpolate(result, value, 0.5)
    }
    result
  }
}

///|
pub fn calibration_focal_aspect(intrinsics : CameraIntrinsics) -> Double {
  if intrinsics.fy.abs() <= 0.000000000001 {
    0.0
  } else {
    intrinsics.fx / intrinsics.fy
  }
}

///|
pub fn calibration_principal_offset(
  intrinsics : CameraIntrinsics,
  size : ImageSize,
) -> Double {
  let center_x = size.width.to_double() * 0.5
  let center_y = size.height.to_double() * 0.5
  ((intrinsics.cx - center_x) * (intrinsics.cx - center_x) +
  (intrinsics.cy - center_y) * (intrinsics.cy - center_y)).sqrt()
}

///|
pub fn calibration_intrinsics_score(
  intrinsics : CameraIntrinsics,
  size : ImageSize,
) -> Double {
  if intrinsics.fx <= 0.0 || intrinsics.fy <= 0.0 {
    0.0
  } else {
    1.0 /
    (
      1.0 +
      calibration_principal_offset(intrinsics, size) +
      (calibration_focal_aspect(intrinsics) - 1.0).abs()
    )
  }
}