///|
fn absolute_value(value : Double) -> Double {
  if value < 0.0 {
    -value
  } else {
    value
  }
}

///|
pub fn SupportContribution::support_index(self : SupportContribution) -> Int {
  self.retained_index
}

///|
pub fn SupportContribution::support_vector(
  self : SupportContribution,
) -> Array[Double] {
  self.retained_row.copy()
}

///|
pub fn SupportContribution::class_label(self : SupportContribution) -> Int {
  self.retained_label
}

///|
pub fn SupportContribution::alpha(self : SupportContribution) -> Double {
  self.retained_alpha
}

///|
pub fn SupportContribution::kernel_value(self : SupportContribution) -> Double {
  self.evaluated_kernel_value
}

///|
pub fn SupportContribution::contribution(self : SupportContribution) -> Double {
  self.signed_decision_contribution
}

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

///|
pub fn BinaryPredictionExplanation::decision_value(
  self : BinaryPredictionExplanation,
) -> Double {
  self.signed_decision_value
}

///|
pub fn BinaryPredictionExplanation::bias(
  self : BinaryPredictionExplanation,
) -> Double {
  self.model_bias
}

///|
pub fn BinaryPredictionExplanation::contributions(
  self : BinaryPredictionExplanation,
) -> Array[SupportContribution] {
  self.ordered_contributions.copy()
}

///|
fn sort_contributions_by_magnitude(
  contributions : Array[SupportContribution],
) -> Unit {
  for index = 1; index < contributions.length(); index = index + 1 {
    let selected = contributions[index]
    let selected_magnitude = absolute_value(selected.contribution())
    let mut position = index
    while position > 0 &&
          absolute_value(contributions[position - 1].contribution()) <
          selected_magnitude {
      contributions[position] = contributions[position - 1]
      position = position - 1
    }
    contributions[position] = selected
  }
}

///|
/// Decomposes a checked binary prediction into support-vector contributions.
pub fn explain_binary_prediction(
  model : BinaryModel,
  row : Array[Double],
) -> Result[BinaryPredictionExplanation, SvmError] {
  let decision = match model.decision_value(row) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let vectors = model.support_vectors()
  let labels = model.support_labels()
  let alphas = model.support_alphas()
  let contributions : Array[SupportContribution] = []
  for index, vector in vectors {
    let evaluated = match kernel_value(model.kernel(), vector, row) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    let sign = if labels[index] == model.positive_class() { 1.0 } else { -1.0 }
    contributions.push({
      retained_index: index,
      retained_row: vector.copy(),
      retained_label: labels[index],
      retained_alpha: alphas[index],
      evaluated_kernel_value: evaluated,
      signed_decision_contribution: sign * alphas[index] * evaluated,
    })
  }
  sort_contributions_by_magnitude(contributions)
  Ok({
    output_class: if decision >= 0.0 {
      model.positive_class()
    } else {
      model.negative_class()
    },
    signed_decision_value: decision,
    model_bias: model.bias(),
    ordered_contributions: contributions,
  })
}

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

///|
pub fn MulticlassPredictionExplanation::classes(
  self : MulticlassPredictionExplanation,
) -> Array[Int] {
  self.ordered_classes.copy()
}

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

///|
/// Returns class-aligned OvR scores and the deterministic winning class.
pub fn explain_multiclass_prediction(
  model : MulticlassModel,
  row : Array[Double],
) -> Result[MulticlassPredictionExplanation, SvmError] {
  let scores = match model.decision_values(row) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let classes = model.classes()
  let mut best = 0
  for index = 1; index < scores.length(); index = index + 1 {
    if scores[index] > scores[best] {
      best = index
    }
  }
  Ok({
    output_class: classes[best],
    ordered_classes: classes,
    ordered_scores: scores,
  })
}