///|
pub fn ThresholdPoint::threshold(self : ThresholdPoint) -> Double {
  self.score_threshold
}

///|
pub fn ThresholdPoint::true_positives(self : ThresholdPoint) -> Int {
  self.accepted_true_positives
}

///|
pub fn ThresholdPoint::false_positives(self : ThresholdPoint) -> Int {
  self.accepted_false_positives
}

///|
pub fn ThresholdPoint::true_negatives(self : ThresholdPoint) -> Int {
  self.rejected_true_negatives
}

///|
pub fn ThresholdPoint::false_negatives(self : ThresholdPoint) -> Int {
  self.rejected_false_negatives
}

///|
pub fn ThresholdPoint::true_positive_rate(self : ThresholdPoint) -> Double {
  self.true_positive_rate_value
}

///|
pub fn ThresholdPoint::false_positive_rate(self : ThresholdPoint) -> Double {
  self.false_positive_rate_value
}

///|
pub fn ThresholdPoint::precision(self : ThresholdPoint) -> Double {
  self.precision_value
}

///|
pub fn ThresholdPoint::recall(self : ThresholdPoint) -> Double {
  self.recall_value
}

///|
pub fn BinaryThresholdAnalysis::points(
  self : BinaryThresholdAnalysis,
) -> Array[ThresholdPoint] {
  self.threshold_points.copy()
}

///|
pub fn BinaryThresholdAnalysis::positive_count(
  self : BinaryThresholdAnalysis,
) -> Int {
  self.observed_positive_count
}

///|
pub fn BinaryThresholdAnalysis::negative_count(
  self : BinaryThresholdAnalysis,
) -> Int {
  self.observed_negative_count
}

///|
pub fn BinaryThresholdAnalysis::roc_auc(
  self : BinaryThresholdAnalysis,
) -> Double {
  self.roc_area
}

///|
pub fn BinaryThresholdAnalysis::average_precision(
  self : BinaryThresholdAnalysis,
) -> Double {
  self.average_precision_value
}

///|
fn descending_score_indices(scores : Array[Double]) -> Array[Int] {
  let indices : Array[Int] = []
  for index = 0; index < scores.length(); index = index + 1 {
    let mut position = 0
    while position < indices.length() &&
          scores[indices[position]] >= scores[index] {
      position = position + 1
    }
    indices.push(index)
    let mut cursor = indices.length() - 1
    while cursor > position {
      indices[cursor] = indices[cursor - 1]
      cursor = cursor - 1
    }
    indices[position] = index
  }
  indices
}

///|
/// Builds tied-threshold confusion evidence and deterministic curve areas.
pub fn binary_threshold_analysis(
  labels : Array[Int],
  scores : Array[Double],
  positive_class : Int,
) -> Result[BinaryThresholdAnalysis, SvmError] {
  if labels.length() != scores.length() {
    return Err(MetricLengthMismatch(labels.length(), scores.length()))
  }
  if labels.is_empty() {
    return Err(EmptyMetricInput)
  }
  let mut positives = 0
  let mut negatives = 0
  for index, score in scores {
    if !finite_double(score) {
      return Err(NonFiniteReportValue("decision score \{index}"))
    }
    if labels[index] == positive_class {
      positives = positives + 1
    } else {
      negatives = negatives + 1
    }
  }
  if positives == 0 || negatives == 0 {
    return Err(SingleClassDataset)
  }
  let order = descending_score_indices(scores)
  let points : Array[ThresholdPoint] = []
  let mut true_positives = 0
  let mut false_positives = 0
  let mut cursor = 0
  let mut previous_tpr = 0.0
  let mut previous_fpr = 0.0
  let mut previous_recall = 0.0
  let mut roc_area = 0.0
  let mut average_precision = 0.0
  while cursor < order.length() {
    let threshold = scores[order[cursor]]
    while cursor < order.length() && scores[order[cursor]] == threshold {
      if labels[order[cursor]] == positive_class {
        true_positives = true_positives + 1
      } else {
        false_positives = false_positives + 1
      }
      cursor = cursor + 1
    }
    let false_negatives = positives - true_positives
    let true_negatives = negatives - false_positives
    let tpr = true_positives.to_double() / positives.to_double()
    let fpr = false_positives.to_double() / negatives.to_double()
    let precision = true_positives.to_double() /
      (true_positives + false_positives).to_double()
    roc_area = roc_area + (fpr - previous_fpr) * (tpr + previous_tpr) / 2.0
    average_precision = average_precision + (tpr - previous_recall) * precision
    points.push({
      score_threshold: threshold,
      accepted_true_positives: true_positives,
      accepted_false_positives: false_positives,
      rejected_true_negatives: true_negatives,
      rejected_false_negatives: false_negatives,
      true_positive_rate_value: tpr,
      false_positive_rate_value: fpr,
      precision_value: precision,
      recall_value: tpr,
    })
    previous_tpr = tpr
    previous_fpr = fpr
    previous_recall = tpr
  }
  Ok({
    threshold_points: points,
    observed_positive_count: positives,
    observed_negative_count: negatives,
    roc_area,
    average_precision_value: average_precision,
  })
}