///|
pub(all) struct NumericSummary {
  count : Int
  min : Double
  max : Double
  mean : Double
  variance : Double
} derive(Debug, ToJson)

///|
pub fn numeric_summary(values : ArrayView[Double]) -> NumericSummary {
  if values.is_empty() {
    return { count: 0, min: 0.0, max: 0.0, mean: 0.0, variance: 0.0 }
  }
  let mut min_value = values[0]
  let mut max_value = values[0]
  let mut sum = 0.0
  for value in values {
    if value < min_value {
      min_value = value
    }
    if value > max_value {
      max_value = value
    }
    sum += value
  }
  let mean = sum / values.length().to_double()
  let mut squared = 0.0
  for value in values {
    let delta = value - mean
    squared += delta * delta
  }
  {
    count: values.length(),
    min: min_value,
    max: max_value,
    mean,
    variance: squared / values.length().to_double(),
  }
}

///|
pub fn percentile(
  values : ArrayView[Double],
  fraction : Double,
) -> Double raise VisionFormatError {
  if values.is_empty() {
    raise VisionFormatError::EmptyInput
  }
  if fraction < 0.0 || fraction > 1.0 {
    raise VisionFormatError::InvalidNumber("percentile must be in [0, 1]")
  }
  let sorted = values.to_owned()
  sorted.sort()
  let index = fraction * (sorted.length() - 1).to_double()
  let low = index.to_int()
  let high = if low + 1 < sorted.length() { low + 1 } else { low }
  sorted[low] + (sorted[high] - sorted[low]) * (index - low.to_double())
}

///|
pub fn median(values : ArrayView[Double]) -> Double raise VisionFormatError {
  percentile(values, 0.5)
}

///|
pub fn mean_absolute_error(
  actual : ArrayView[Double],
  predicted : ArrayView[Double],
) -> Double raise VisionFormatError {
  if actual.length() != predicted.length() {
    raise VisionFormatError::InvalidShape(
      "metric vectors must have equal length",
    )
  }
  if actual.is_empty() {
    raise VisionFormatError::EmptyInput
  }
  let mut total = 0.0
  for i in 0.. Double raise VisionFormatError {
  if actual.length() != predicted.length() || actual.is_empty() {
    raise VisionFormatError::InvalidShape(
      "metric vectors must have equal non-zero length",
    )
  }
  let mut total = 0.0
  for i in 0.. (Double, Double, Double) {
  let precision = if true_positive + false_positive == 0 {
    0.0
  } else {
    true_positive.to_double() / (true_positive + false_positive).to_double()
  }
  let recall = if true_positive + false_negative == 0 {
    0.0
  } else {
    true_positive.to_double() / (true_positive + false_negative).to_double()
  }
  let f1 = if precision + recall == 0.0 {
    0.0
  } else {
    2.0 * precision * recall / (precision + recall)
  }
  (precision, recall, f1)
}

///|
pub fn average_precision_at_iou(
  predictions : ArrayView[BoundingBox],
  ground_truth : ArrayView[BoundingBox],
  threshold : Double,
) -> Double {
  if ground_truth.is_empty() {
    return 0.0
  }
  let mut matched = 0
  let used : Array[Bool] = Array::make(ground_truth.length(), false)
  for prediction in predictions {
    let mut best = -1
    let mut best_iou = threshold
    for i in 0..= best_iou {
        best = i
        best_iou = score
      }
    }
    if best >= 0 {
      used[best] = true
      matched += 1
    }
  }
  matched.to_double() / predictions.length().to_double()
}

///|
pub fn box_recall_at_iou(
  predictions : ArrayView[BoundingBox],
  ground_truth : ArrayView[BoundingBox],
  threshold : Double,
) -> Double {
  if ground_truth.is_empty() {
    return 1.0
  }
  let mut count = 0
  for target in ground_truth {
    if predictions.any(fn(candidate) { candidate.iou(target) >= threshold }) {
      count += 1
    }
  }
  count.to_double() / ground_truth.length().to_double()
}

///|
pub fn annotation_area_histogram(
  annotations : ArrayView[Annotation],
  bins : Int,
) -> Array[Int] {
  let result : Array[Int] = Array::make(if bins > 0 { bins } else { 0 }, 0)
  if bins <= 0 {
    return result
  }
  for annotation in annotations {
    let area = annotation.bbox.area()
    let index = if area.to_int() >= bins { bins - 1 } else { area.to_int() }
    result[index] += 1
  }
  result
}

///|
pub fn label_counts(annotations : ArrayView[Annotation]) -> Map[String, Int] {
  let result : Map[String, Int] = Map([])
  for annotation in annotations {
    if result.contains(annotation.label) {
      result[annotation.label] = result[annotation.label] + 1
    } else {
      result[annotation.label] = 1
    }
  }
  result
}

///|
pub fn confidence_summary(
  annotations : ArrayView[Annotation],
) -> NumericSummary {
  let values = annotations.map(fn(annotation) { annotation.confidence })
  numeric_summary(values)
}