///|
pub fn RepeatedValidationReport::repetition_count(
  self : RepeatedValidationReport,
) -> Int {
  self.round_reports.length()
}

///|
pub fn RepeatedValidationReport::fold_count(
  self : RepeatedValidationReport,
) -> Int {
  self.folds_per_round
}

///|
pub fn RepeatedValidationReport::reports(
  self : RepeatedValidationReport,
) -> Array[ValidationReport] {
  self.round_reports.copy()
}

///|
pub fn RepeatedValidationReport::mean_accuracy(
  self : RepeatedValidationReport,
) -> Double {
  self.accuracy_mean
}

///|
pub fn RepeatedValidationReport::minimum_accuracy(
  self : RepeatedValidationReport,
) -> Double {
  self.accuracy_minimum
}

///|
pub fn RepeatedValidationReport::maximum_accuracy(
  self : RepeatedValidationReport,
) -> Double {
  self.accuracy_maximum
}

///|
pub fn RepeatedValidationReport::accuracy_standard_deviation(
  self : RepeatedValidationReport,
) -> Double {
  self.accuracy_deviation
}

///|
pub fn RepeatedValidationReport::mean_macro_f1(
  self : RepeatedValidationReport,
) -> Double {
  self.macro_f1_mean
}

///|
pub fn RepeatedValidationReport::minimum_macro_f1(
  self : RepeatedValidationReport,
) -> Double {
  self.macro_f1_minimum
}

///|
pub fn RepeatedValidationReport::maximum_macro_f1(
  self : RepeatedValidationReport,
) -> Double {
  self.macro_f1_maximum
}

///|
pub fn RepeatedValidationReport::macro_f1_standard_deviation(
  self : RepeatedValidationReport,
) -> Double {
  self.macro_f1_deviation
}

///|
fn stratified_round_folds(
  data : Dataset,
  fold_count : Int,
  round : Int,
) -> Result[Array[StratifiedFold], SvmError] {
  if fold_count < 2 || fold_count > data.row_count() {
    return Err(InvalidFoldCount(fold_count))
  }
  let classes = data.classes()
  for label in classes {
    let count = data.class_count(label)
    if count < fold_count {
      return Err(InsufficientClassSamples(label, count, fold_count))
    }
  }
  let tests : Array[Array[Int]] = Array::makei(fold_count, fn(_) { [] })
  let labels = data.labels()
  for class_index, label in classes {
    let mut occurrence = 0
    let offset = round * (class_index + 1) % fold_count
    for row, actual in labels {
      if actual == label {
        tests[(occurrence + offset) % fold_count].push(row)
        occurrence = occurrence + 1
      }
    }
  }
  let folds : Array[StratifiedFold] = []
  for fold_index = 0; fold_index < fold_count; fold_index = fold_index + 1 {
    let train : Array[Int] = []
    for row = 0; row < data.row_count(); row = row + 1 {
      if !contains_index(tests[fold_index], row) {
        train.push(row)
      }
    }
    folds.push({
      fold_index,
      training_indices: train,
      testing_indices: tests[fold_index].copy(),
    })
  }
  Ok(folds)
}

///|
fn evaluate_partition_set(
  data : Dataset,
  config : BinaryConfig,
  partitions : Array[StratifiedFold],
  scaling : ScalingPlan,
) -> Result[ValidationReport, SvmError] {
  let evaluations : Array[FoldEvaluation] = []
  let aggregate_actual : Array[Int] = []
  let aggregate_predicted : Array[Int] = []
  for partition in partitions {
    let training_data = match
      data.subset(partition.train_indices(), "repeated training fold") {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    let testing_data = match
      data.subset(partition.test_indices(), "repeated testing fold") {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    let (fit_data, predict_data, scaler) = match
      scaled_fold_data(training_data, testing_data, scaling) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    let model = match train_multiclass(fit_data, config) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    let actual = predict_data.labels()
    let predicted = match model.predict_batch(predict_data.features()) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    let metrics = match classification_metrics(actual, predicted) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    for label in actual {
      aggregate_actual.push(label)
    }
    for label in predicted {
      aggregate_predicted.push(label)
    }
    evaluations.push({
      fold_number: partition.index(),
      training_rows: partition.train_indices(),
      testing_rows: partition.test_indices(),
      actual_labels: actual,
      predicted_labels: predicted,
      fold_metrics: metrics,
      fitted_scaler: scaler,
    })
  }
  let aggregate = match
    classification_metrics(aggregate_actual, aggregate_predicted) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  Ok({
    fold_evaluations: evaluations,
    aggregate_metrics: aggregate,
    evaluated_observations: data.row_count(),
    scaling_plan: scaling,
  })
}

///|
fn numeric_summary(values : Array[Double]) -> (Double, Double, Double, Double) {
  let mut minimum = values[0]
  let mut maximum = values[0]
  let mut total = 0.0
  for value in values {
    if value < minimum {
      minimum = value
    }
    if value > maximum {
      maximum = value
    }
    total = total + value
  }
  let mean = total / values.length().to_double()
  let mut squared_total = 0.0
  for value in values {
    let difference = value - mean
    squared_total = squared_total + difference * difference
  }
  (mean, minimum, maximum, (squared_total / values.length().to_double()).sqrt())
}

///|
/// Repeats deterministic stratified CV with class-specific fold rotations.
pub fn repeated_cross_validate(
  data : Dataset,
  config : BinaryConfig,
  fold_count : Int,
  repetitions : Int,
  scaling : ScalingPlan,
) -> Result[RepeatedValidationReport, SvmError] {
  if repetitions <= 0 {
    return Err(InvalidIterationCount(repetitions))
  }
  let reports : Array[ValidationReport] = []
  let accuracies : Array[Double] = []
  let macro_f1_values : Array[Double] = []
  for round = 0; round < repetitions; round = round + 1 {
    let partitions = match stratified_round_folds(data, fold_count, round) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    let report = match
      evaluate_partition_set(data, config, partitions, scaling) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    accuracies.push(report.metrics().accuracy())
    macro_f1_values.push(report.metrics().macro_f1())
    reports.push(report)
  }
  let (accuracy_mean, accuracy_minimum, accuracy_maximum, accuracy_deviation) = numeric_summary(
    accuracies,
  )
  let (macro_f1_mean, macro_f1_minimum, macro_f1_maximum, macro_f1_deviation) = numeric_summary(
    macro_f1_values,
  )
  Ok({
    round_reports: reports,
    folds_per_round: fold_count,
    accuracy_mean,
    accuracy_minimum,
    accuracy_maximum,
    accuracy_deviation,
    macro_f1_mean,
    macro_f1_minimum,
    macro_f1_maximum,
    macro_f1_deviation,
  })
}