///|
pub(all) struct LabelSummary {
  label : String
  count : Int
  mean_confidence : Double
  total_area : Double
} derive(Debug, ToJson)

///|
pub fn summarize_labels(
  annotations : ArrayView[Annotation],
) -> Array[LabelSummary] {
  let labels : Array[String] = []
  for annotation in annotations {
    if !labels.contains(annotation.label) {
      labels.push(annotation.label)
    }
  }
  labels.map(fn(label) {
    let selected = annotations.filter(fn(item) { item.label == label })
    let confidence = selected.map(fn(item) { item.confidence })
    let areas = selected.map(fn(item) { item.bbox.area() })
    let confidence_stats = numeric_summary(confidence)
    {
      label,
      count: selected.length(),
      mean_confidence: confidence_stats.mean,
      total_area: areas.fold(init=0.0, fn(total, value) { total + value }),
    }
  })
}

///|
pub fn annotations_for_label(
  annotations : ArrayView[Annotation],
  label : String,
) -> Array[Annotation] {
  annotations.filter(fn(annotation) { annotation.label == label })
}

///|
pub fn annotations_in_region(
  annotations : ArrayView[Annotation],
  region : BoundingBox,
) -> Array[Annotation] {
  annotations.filter(fn(annotation) {
    annotation.bbox.intersection_area(region) > 0.0
  })
}

///|
pub fn annotations_at_stamp(
  annotations : ArrayView[Annotation],
  stamp : Stamp,
  tolerance_ns : Int64,
) -> Array[Annotation] {
  annotations.filter(fn(annotation) {
    abs_i64(annotation.stamp.to_nanoseconds() - stamp.to_nanoseconds()) <=
    tolerance_ns
  })
}

///|
pub fn greedy_non_max_suppression(
  annotations : ArrayView[Annotation],
  iou_threshold : Double,
) -> Array[Annotation] {
  let ordered = annotations.to_owned()
  ordered.sort_by(fn(a, b) { b.confidence.compare(a.confidence) })
  let selected : Array[Annotation] = []
  for candidate in ordered {
    if selected.all(fn(previous) {
        previous.label != candidate.label ||
        previous.bbox.iou(candidate.bbox) < iou_threshold
      }) {
      selected.push(candidate)
    }
  }
  selected
}

///|
pub fn annotation_centroid(annotation : Annotation) -> Point2D {
  {
    x: annotation.bbox.x + annotation.bbox.width / 2.0,
    y: annotation.bbox.y + annotation.bbox.height / 2.0,
  }
}

///|
pub fn annotation_centroids(
  annotations : ArrayView[Annotation],
) -> Array[Point2D] {
  annotations.map(annotation_centroid)
}

///|
pub fn annotation_density(
  annotations : ArrayView[Annotation],
  width : Int,
  height : Int,
) -> Double {
  let area = width.to_double() * height.to_double()
  if area <= 0.0 {
    0.0
  } else {
    annotations.fold(init=0.0, fn(total, annotation) {
      total + annotation.bbox.area()
    }) /
    area
  }
}

///|
pub fn validate_label_name(label : String) -> Bool {
  if label.is_empty() {
    false
  } else {
    label.trim() == label &&
    !label.contains(" ") &&
    !label.contains("\t") &&
    !label.contains(",")
  }
}

///|
pub fn invalid_label_names(
  annotations : ArrayView[Annotation],
) -> Array[String] {
  let result = Array::new()
  for annotation in annotations {
    if !validate_label_name(annotation.label) &&
      !result.contains(annotation.label) {
      result.push(annotation.label)
    }
  }
  result
}