///|
pub fn PredictionObservation::row_index(self : PredictionObservation) -> Int {
  self.source_row_index
}

///|
pub fn PredictionObservation::actual_class(self : PredictionObservation) -> Int {
  self.observed_class
}

///|
pub fn PredictionObservation::predicted_class(
  self : PredictionObservation,
) -> Int {
  self.selected_class
}

///|
pub fn PredictionObservation::winning_score(
  self : PredictionObservation,
) -> Double {
  self.selected_score
}

///|
pub fn PredictionObservation::second_class(self : PredictionObservation) -> Int {
  self.runner_up_class
}

///|
pub fn PredictionObservation::second_score(
  self : PredictionObservation,
) -> Double {
  self.runner_up_score
}

///|
pub fn PredictionObservation::winning_margin(
  self : PredictionObservation,
) -> Double {
  self.selected_margin
}

///|
pub fn PredictionObservation::scores(
  self : PredictionObservation,
) -> Array[Double] {
  self.ordered_scores.copy()
}

///|
pub fn PredictionObservation::is_correct(self : PredictionObservation) -> Bool {
  self.prediction_correct
}

///|
pub fn ClassScoreProfile::class_label(self : ClassScoreProfile) -> Int {
  self.profiled_class
}

///|
pub fn ClassScoreProfile::positive_count(self : ClassScoreProfile) -> Int {
  self.positive_observations
}

///|
pub fn ClassScoreProfile::negative_count(self : ClassScoreProfile) -> Int {
  self.negative_observations
}

///|
pub fn ClassScoreProfile::minimum_positive_score(
  self : ClassScoreProfile,
) -> Double {
  self.positive_score_minimum
}

///|
pub fn ClassScoreProfile::maximum_positive_score(
  self : ClassScoreProfile,
) -> Double {
  self.positive_score_maximum
}

///|
pub fn ClassScoreProfile::positive_mean_score(
  self : ClassScoreProfile,
) -> Double {
  self.positive_score_mean
}

///|
pub fn ClassScoreProfile::minimum_negative_score(
  self : ClassScoreProfile,
) -> Double {
  self.negative_score_minimum
}

///|
pub fn ClassScoreProfile::maximum_negative_score(
  self : ClassScoreProfile,
) -> Double {
  self.negative_score_maximum
}

///|
pub fn ClassScoreProfile::negative_mean_score(
  self : ClassScoreProfile,
) -> Double {
  self.negative_score_mean
}

///|
pub fn ClassScoreProfile::score_separation(self : ClassScoreProfile) -> Double {
  self.mean_score_separation
}

///|
pub fn PredictionAudit::observation_count(self : PredictionAudit) -> Int {
  self.audited_observations.length()
}

///|
pub fn PredictionAudit::correct_count(self : PredictionAudit) -> Int {
  self.correct_prediction_count
}

///|
pub fn PredictionAudit::incorrect_count(self : PredictionAudit) -> Int {
  self.incorrect_prediction_count
}

///|
pub fn PredictionAudit::accuracy(self : PredictionAudit) -> Double {
  self.audit_metrics.accuracy()
}

///|
pub fn PredictionAudit::metrics(
  self : PredictionAudit,
) -> ClassificationMetrics {
  self.audit_metrics
}

///|
pub fn PredictionAudit::observations(
  self : PredictionAudit,
) -> Array[PredictionObservation] {
  self.audited_observations.copy()
}

///|
pub fn PredictionAudit::misclassified_observations(
  self : PredictionAudit,
) -> Array[PredictionObservation] {
  let result : Array[PredictionObservation] = []
  for observation in self.audited_observations {
    if !observation.is_correct() {
      result.push(observation)
    }
  }
  result
}

///|
pub fn PredictionAudit::class_score_profiles(
  self : PredictionAudit,
) -> Array[ClassScoreProfile] {
  self.audited_class_profiles.copy()
}

///|
pub fn PredictionAudit::minimum_winning_margin(
  self : PredictionAudit,
) -> Double {
  self.smallest_winning_margin
}

///|
pub fn PredictionAudit::maximum_winning_margin(
  self : PredictionAudit,
) -> Double {
  self.largest_winning_margin
}

///|
pub fn PredictionAudit::mean_winning_margin(self : PredictionAudit) -> Double {
  self.average_winning_margin
}

///|
fn audit_class_index(classes : Array[Int], label : Int) -> Int? {
  for index, candidate in classes {
    if candidate == label {
      return Some(index)
    }
  }
  None
}

///|
fn winning_score_indices(scores : Array[Double]) -> (Int, Int) {
  let mut best = 0
  let mut second = 1
  if scores[second] > scores[best] {
    let temporary = best
    best = second
    second = temporary
  }
  for index = 2; index < scores.length(); index = index + 1 {
    if scores[index] > scores[best] {
      second = best
      best = index
    } else if scores[index] > scores[second] {
      second = index
    }
  }
  (best, second)
}

///|
fn class_score_profile(
  class_index : Int,
  classes : Array[Int],
  labels : Array[Int],
  observations : Array[PredictionObservation],
) -> Result[ClassScoreProfile, SvmError] {
  let label = classes[class_index]
  let mut positive_count = 0
  let mut negative_count = 0
  let mut positive_minimum = 0.0
  let mut positive_maximum = 0.0
  let mut negative_minimum = 0.0
  let mut negative_maximum = 0.0
  let mut positive_total = 0.0
  let mut negative_total = 0.0
  for row, actual in labels {
    let score = observations[row].ordered_scores[class_index]
    if actual == label {
      if positive_count == 0 || score < positive_minimum {
        positive_minimum = score
      }
      if positive_count == 0 || score > positive_maximum {
        positive_maximum = score
      }
      positive_total = positive_total + score
      positive_count = positive_count + 1
    } else {
      if negative_count == 0 || score < negative_minimum {
        negative_minimum = score
      }
      if negative_count == 0 || score > negative_maximum {
        negative_maximum = score
      }
      negative_total = negative_total + score
      negative_count = negative_count + 1
    }
  }
  if positive_count == 0 {
    return Err(InsufficientClassSamples(label, 0, 1))
  }
  if negative_count == 0 {
    return Err(SingleClassDataset)
  }
  let positive_mean = positive_total / positive_count.to_double()
  let negative_mean = negative_total / negative_count.to_double()
  Ok({
    profiled_class: label,
    positive_observations: positive_count,
    negative_observations: negative_count,
    positive_score_minimum: positive_minimum,
    positive_score_maximum: positive_maximum,
    positive_score_mean: positive_mean,
    negative_score_minimum: negative_minimum,
    negative_score_maximum: negative_maximum,
    negative_score_mean: negative_mean,
    mean_score_separation: positive_mean - negative_mean,
  })
}

///|
/// Audits every OvR prediction and class score on a compatible dataset.
pub fn multiclass_prediction_audit(
  model : MulticlassModel,
  data : Dataset,
) -> Result[PredictionAudit, SvmError] {
  if data.feature_count() != model.feature_count() {
    return Err(
      PredictionDimensionMismatch(model.feature_count(), data.feature_count()),
    )
  }
  let classes = model.classes()
  let labels = data.labels()
  for label in labels {
    if audit_class_index(classes, label) is None {
      return Err(UnknownClassLabel(label))
    }
  }
  let observations : Array[PredictionObservation] = []
  let predictions : Array[Int] = []
  let mut correct = 0
  let mut incorrect = 0
  let mut minimum_margin = 0.0
  let mut maximum_margin = 0.0
  let mut margin_total = 0.0
  for row_index, row in data.features() {
    let scores = match model.decision_values(row) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    let (best, second) = winning_score_indices(scores)
    let predicted = classes[best]
    let margin = scores[best] - scores[second]
    let is_correct = predicted == labels[row_index]
    if is_correct {
      correct = correct + 1
    } else {
      incorrect = incorrect + 1
    }
    if row_index == 0 || margin < minimum_margin {
      minimum_margin = margin
    }
    if row_index == 0 || margin > maximum_margin {
      maximum_margin = margin
    }
    margin_total = margin_total + margin
    predictions.push(predicted)
    observations.push({
      source_row_index: row_index,
      observed_class: labels[row_index],
      selected_class: predicted,
      selected_score: scores[best],
      runner_up_class: classes[second],
      runner_up_score: scores[second],
      selected_margin: margin,
      ordered_scores: scores,
      prediction_correct: is_correct,
    })
  }
  let metrics = match classification_metrics(labels, predictions) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let profiles : Array[ClassScoreProfile] = []
  for class_index = 0
      class_index < classes.length()
      class_index = class_index + 1 {
    match class_score_profile(class_index, classes, labels, observations) {
      Err(error) => return Err(error)
      Ok(value) => profiles.push(value)
    }
  }
  Ok({
    audited_observations: observations,
    audited_class_profiles: profiles,
    audit_metrics: metrics,
    correct_prediction_count: correct,
    incorrect_prediction_count: incorrect,
    smallest_winning_margin: minimum_margin,
    largest_winning_margin: maximum_margin,
    average_winning_margin: margin_total / data.row_count().to_double(),
  })
}