///|
pub struct BinaryMetrics {
  true_positive : Int
  false_positive : Int
  false_negative : Int
  true_negative : Int
} derive(Debug, Eq)

///|
pub fn BinaryMetrics::new(
  true_positive~ : Int,
  false_positive~ : Int,
  false_negative~ : Int,
  true_negative~ : Int,
) -> BinaryMetrics {
  { true_positive, false_positive, false_negative, true_negative }
}

///|
pub fn BinaryMetrics::precision(self : BinaryMetrics) -> Int {
  let denominator = self.true_positive + self.false_positive
  if denominator == 0 {
    1000
  } else {
    self.true_positive * 1000 / denominator
  }
}

///|
pub fn BinaryMetrics::recall(self : BinaryMetrics) -> Int {
  let denominator = self.true_positive + self.false_negative
  if denominator == 0 {
    1000
  } else {
    self.true_positive * 1000 / denominator
  }
}

///|
pub fn BinaryMetrics::specificity(self : BinaryMetrics) -> Int {
  let denominator = self.true_negative + self.false_positive
  if denominator == 0 {
    1000
  } else {
    self.true_negative * 1000 / denominator
  }
}

///|
pub fn BinaryMetrics::f1(self : BinaryMetrics) -> Int {
  let p = self.precision()
  let r = self.recall()
  if p + r == 0 {
    0
  } else {
    2 * p * r / (p + r)
  }
}

///|
pub fn evaluate_mask(
  expected : Mask,
  actual : Mask,
  positive : Int,
) -> BinaryMetrics {
  let count = min_i(expected.values.length(), actual.values.length())
  let mut tp = 0
  let mut fp = 0
  let mut fn_ = 0
  let mut tn = 0
  for i in 0.. Array[DetectionMatch] {
  let out : Array[DetectionMatch] = []
  for expected_box in expected {
    let mut best_id = -1
    let mut best_iou = 0
    for actual_box in actual {
      let iou = expected_box.iou(actual_box)
      if iou >= threshold_per_1000 && iou > best_iou {
        best_iou = iou
        best_id = actual_box.label.id
      }
    }
    if best_id >= 0 {
      out.push(
        DetectionMatch::new(
          expected_id=expected_box.label.id,
          actual_id=best_id,
          iou_per_1000=best_iou,
        ),
      )
    }
  }
  out
}

///|
pub fn DetectionMatch::new(
  expected_id~ : Int,
  actual_id~ : Int,
  iou_per_1000~ : Int,
) -> DetectionMatch {
  { expected_id, actual_id, iou_per_1000 }
}

///|
pub struct DetectionMetrics {
  matches : Int
  expected : Int
  actual : Int
  mean_iou_per_1000 : Int
} derive(Debug, Eq)

///|
pub fn evaluate_detections(
  expected : Array[Box2D],
  actual : Array[Box2D],
  threshold_per_1000 : Int,
) -> DetectionMetrics {
  let matches = match_detections(expected, actual, threshold_per_1000)
  let mut total_iou = 0
  for item in matches {
    total_iou += item.iou_per_1000
  }
  {
    matches: matches.length(),
    expected: expected.length(),
    actual: actual.length(),
    mean_iou_per_1000: if matches.length() == 0 {
      0
    } else {
      total_iou / matches.length()
    },
  }
}

///|
pub fn DetectionMetrics::precision(self : DetectionMetrics) -> Int {
  if self.actual == 0 {
    1000
  } else {
    self.matches * 1000 / self.actual
  }
}

///|
pub fn DetectionMetrics::recall(self : DetectionMetrics) -> Int {
  if self.expected == 0 {
    1000
  } else {
    self.matches * 1000 / self.expected
  }
}

///|
pub fn DetectionMetrics::to_json(self : DetectionMetrics) -> String {
  "{\"matches\":\{self.matches},\"expected\":\{self.expected},\"actual\":\{self.actual},\"mean_iou_per_1000\":\{self.mean_iou_per_1000},\"precision_per_1000\":\{self.precision()},\"recall_per_1000\":\{self.recall()}}"
}