///|
pub(all) struct Evaluation {
  count : Int
  weight : Double
  mse : Double
  mae : Double
  bias : Double
  maximum_error : Double
  loss : Double
} derive(Debug)

///|
pub fn evaluate_predictions(
  y : Array[Double],
  predictions : Array[Double],
  weights : Array[Double],
  loss? : Loss = Squared,
) -> Result[Evaluation, String] {
  match validate_series(y, weights) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  if predictions.length() != y.length() {
    return Err("prediction length mismatch")
  }
  let mut total_weight = 0.0
  let mut squared = 0.0
  let mut absolute = 0.0
  let mut bias = 0.0
  let mut maximum_error = 0.0
  for i = 0; i < y.length(); i = i + 1 {
    if !finite(predictions[i]) {
      return Err("nonfinite prediction")
    }
    let error = predictions[i] - y[i]
    if !finite(error) {
      return Err("error overflow")
    }
    total_weight = total_weight + weights[i]
    squared = squared + weights[i] * error * error
    absolute = absolute + weights[i] * error.abs()
    bias = bias + weights[i] * error
    if error.abs() > maximum_error {
      maximum_error = error.abs()
    }
    if !finite(squared) || !finite(absolute) || !finite(bias) {
      return Err("evaluation overflow")
    }
  }
  let objective = match loss {
    Squared => squared
    Absolute => absolute
    Quantile(tau) =>
      match pinball_loss(y, predictions, weights, tau) {
        Err(e) => return Err(e)
        Ok(v) => v
      }
  }
  Ok({
    count: y.length(),
    weight: total_weight,
    mse: squared / total_weight,
    mae: absolute / total_weight,
    bias: bias / total_weight,
    maximum_error,
    loss: objective / total_weight,
  })
}

///|
pub fn evaluate(
  model : Model,
  samples : Array[Sample],
) -> Result[Evaluation, String] {
  match validate_samples(samples) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  let predictions = match predict_many(model, samples.map(fn(s) { s.x })) {
    Err(e) => return Err(e)
    Ok(v) => v
  }
  evaluate_predictions(
    samples.map(fn(s) { s.y }),
    predictions,
    samples.map(fn(s) { s.weight }),
    loss=model.loss,
  )
}

///|
pub fn evaluation_lines(evaluation : Evaluation) -> Array[String] {
  [
    "count=" + evaluation.count.to_string(),
    "weight=" + evaluation.weight.to_string(),
    "mse=" + evaluation.mse.to_string(),
    "mae=" + evaluation.mae.to_string(),
    "bias=" + evaluation.bias.to_string(),
    "maximum_error=" + evaluation.maximum_error.to_string(),
    "loss=" + evaluation.loss.to_string(),
  ]
}

///|
#deprecated
pub extend Evaluation with @debug.Debug::{to_repr}