///|
/// A paired metric difference. Undefined COCO values are JSON null here.
pub struct MetricChange {
  name : String
  baseline : Double?
  candidate : Double?
  delta : Double?
}

///|
pub impl ToJson for MetricChange with fn to_json(self : MetricChange) -> Json {
  let fields : Map[String, Json] = Map([])
  fields["name"] = Json::string(self.name)
  fields["baseline"] = optional_metric_json(self.baseline)
  fields["candidate"] = optional_metric_json(self.candidate)
  fields["delta"] = optional_metric_json(self.delta)
  Json::object(fields)
}

///|
pub extend MetricChange with ToJson::{to_json}

///|
fn optional_metric_json(value : Double?) -> Json {
  match value {
    Some(number) => Json::number(number)
    None => Json::null()
  }
}

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

///|
pub fn MetricChange::baseline(self : MetricChange) -> Double? {
  self.baseline
}

///|
pub fn MetricChange::candidate(self : MetricChange) -> Double? {
  self.candidate
}

///|
pub fn MetricChange::delta(self : MetricChange) -> Double? {
  self.delta
}

///|
/// Category values use the same metric order as the overall comparison.
pub struct ClassComparison {
  category_id : Int
  metrics : Array[MetricChange]
} derive(ToJson)

///|
pub extend ClassComparison with ToJson::{to_json}

///|
pub fn ClassComparison::category_id(self : ClassComparison) -> Int {
  self.category_id
}

///|
pub fn ClassComparison::metrics(self : ClassComparison) -> Array[MetricChange] {
  self.metrics.copy()
}

///|
/// Both evaluations share one dataset and one configuration.
pub struct ComparisonReport {
  schema_version : String
  baseline : EvaluationReport
  candidate : EvaluationReport
  metrics : Array[MetricChange]
  classes : Array[ClassComparison]
  image_changes : Array[ImageChange]
} derive(ToJson)

///|
/// Candidate minus baseline non-ignored errors at IoU=0.50.
pub struct ImageChange {
  image_id : Int
  baseline_errors : Int
  candidate_errors : Int
  error_delta : Int
} derive(ToJson)

///|
pub extend ImageChange with ToJson::{to_json}

///|
pub fn ImageChange::image_id(self : ImageChange) -> Int {
  self.image_id
}

///|
pub fn ImageChange::baseline_errors(self : ImageChange) -> Int {
  self.baseline_errors
}

///|
pub fn ImageChange::candidate_errors(self : ImageChange) -> Int {
  self.candidate_errors
}

///|
pub fn ImageChange::error_delta(self : ImageChange) -> Int {
  self.error_delta
}

///|
pub extend ComparisonReport with ToJson::{to_json}

///|
pub fn ComparisonReport::to_json_string(self : ComparisonReport) -> String {
  @json.to_json(self).stringify()
}

///|
pub fn ComparisonReport::baseline(self : ComparisonReport) -> EvaluationReport {
  self.baseline
}

///|
pub fn ComparisonReport::candidate(self : ComparisonReport) -> EvaluationReport {
  self.candidate
}

///|
pub fn ComparisonReport::metrics(
  self : ComparisonReport,
) -> Array[MetricChange] {
  self.metrics.copy()
}

///|
pub fn ComparisonReport::classes(
  self : ComparisonReport,
) -> Array[ClassComparison] {
  self.classes.copy()
}

///|
pub fn ComparisonReport::image_changes(
  self : ComparisonReport,
) -> Array[ImageChange] {
  self.image_changes.copy()
}

///|
fn metric_change(
  name : String,
  baseline : Double,
  candidate : Double,
) -> MetricChange {
  let left = if baseline < 0.0 { None } else { Some(baseline) }
  let right = if candidate < 0.0 { None } else { Some(candidate) }
  {
    name,
    baseline: left,
    candidate: right,
    delta: if baseline < 0.0 || candidate < 0.0 {
      None
    } else {
      Some(candidate - baseline)
    },
  }
}

///|
fn metric_changes(
  baseline : Metrics,
  candidate : Metrics,
) -> Array[MetricChange] {
  [
    metric_change("ap", baseline.ap, candidate.ap),
    metric_change("ap50", baseline.ap50, candidate.ap50),
    metric_change("ap75", baseline.ap75, candidate.ap75),
    metric_change("ap_small", baseline.ap_small, candidate.ap_small),
    metric_change("ap_medium", baseline.ap_medium, candidate.ap_medium),
    metric_change("ap_large", baseline.ap_large, candidate.ap_large),
    metric_change("ar1", baseline.ar1, candidate.ar1),
    metric_change("ar10", baseline.ar10, candidate.ar10),
    metric_change("ar100", baseline.ar100, candidate.ar100),
    metric_change("ar_small", baseline.ar_small, candidate.ar_small),
    metric_change("ar_medium", baseline.ar_medium, candidate.ar_medium),
    metric_change("ar_large", baseline.ar_large, candidate.ar_large),
  ]
}

///|
/// Compare two prediction arrays against exactly the same ground truths.
pub fn compare_predictions(
  dataset : Dataset,
  baseline : Array[Detection],
  candidate : Array[Detection],
) -> ComparisonReport raise EvalError {
  compare_predictions_with_config(
    dataset,
    baseline,
    candidate,
    EvalConfig::coco(),
  )
}

///|
/// Compare with a shared IoU/diagnostic/ignore configuration.
pub fn compare_predictions_with_config(
  dataset : Dataset,
  baseline : Array[Detection],
  candidate : Array[Detection],
  config : EvalConfig,
) -> ComparisonReport raise EvalError {
  let left = evaluate_with_config(dataset, baseline, config)
  let right = evaluate_with_config(dataset, candidate, config)
  let classes : Array[ClassComparison] = []
  for index in 0.. right.error_delta {
      -1
    } else if left.error_delta < right.error_delta {
      1
    } else if left.image_id < right.image_id {
      -1
    } else if left.image_id > right.image_id {
      1
    } else {
      0
    }
  })
  {
    schema_version: "0.1.0",
    baseline: left,
    candidate: right,
    metrics: metric_changes(left.summary, right.summary),
    classes,
    image_changes,
  }
}