///|
pub struct ResidualMetrics {
  count : Int
  mean_absolute_error : Double
  rmse : Double
  max_abs_error : Double
  max_relative_error : Double
  signed_bias : Double
} derive(Debug)

///|
pub struct GridSpacing {
  count : Int
  minimum : Double
  maximum : Double
  mean : Double
} derive(Debug)

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

///|
fn max_or_zero(first : Double, second : Double) -> Double {
  if first > second {
    first
  } else {
    second
  }
}

///|
fn finite_relative_error(reference : Double, predicted : Double) -> Double {
  let scale = max_or_zero(positive_or_zero(reference), 1.0E-12)
  positive_or_zero(predicted - reference) / scale
}

///|
pub fn residual_metrics(
  references : Array[Double],
  predicted : Array[Double],
) -> ResidualMetrics {
  if references.length() != predicted.length() || references.length() == 0 {
    ResidualMetrics::{
      count: 0,
      mean_absolute_error: 0.0,
      rmse: 0.0,
      max_abs_error: 0.0,
      max_relative_error: 0.0,
      signed_bias: 0.0,
    }
  } else {
    let (absolute, squared, maximum, relative, bias) = for i = 0, absolute = 0.0, squared = 0.0, maximum = 0.0, relative = 0.0, bias = 0.0; i <
                                                          references.length(); {
      let error = predicted[i] - references[i]
      let absolute_error = positive_or_zero(error)
      let relative_error = finite_relative_error(references[i], predicted[i])
      continue i + 1,
        absolute + absolute_error,
        squared + error * error,
        max_or_zero(maximum, absolute_error),
        max_or_zero(relative, relative_error),
        bias + error
    } nobreak {
      (absolute, squared, maximum, relative, bias)
    }
    let count = references.length().to_double()
    ResidualMetrics::{
      count: references.length(),
      mean_absolute_error: absolute / count,
      rmse: @math.pow(squared / count, 0.5),
      max_abs_error: maximum,
      max_relative_error: relative,
      signed_bias: bias / count,
    }
  }
}

///|
pub fn quality_gate(
  metrics : ResidualMetrics,
  max_rmse~ : Double,
  max_relative_error~ : Double,
) -> Bool {
  metrics.count > 0 &&
  max_rmse >= 0.0 &&
  max_relative_error >= 0.0 &&
  metrics.rmse <= max_rmse &&
  metrics.max_relative_error <= max_relative_error
}

///|
pub fn residual_vector(
  references : Array[Double],
  predicted : Array[Double],
) -> Array[Double] {
  if references.length() != predicted.length() {
    []
  } else {
    [
      for i in 0.. predicted[i] - references[i]
    ]
  }
}

///|
pub fn max_abs_value(values : Array[Double]) -> Double {
  for i = 0, maximum = 0.0; i < values.length(); {
    continue i + 1, max_or_zero(maximum, positive_or_zero(values[i]))
  } nobreak {
    maximum
  }
}

///|
pub fn monotonicity_violations(
  values : Array[Double],
  increasing~ : Bool,
) -> Int {
  if values.length() < 2 {
    0
  } else {
    for i = 1, count = 0; i < values.length(); {
      let violates = if increasing {
        values[i] < values[i - 1]
      } else {
        values[i] > values[i - 1]
      }
      continue i + 1, if violates { count + 1 } else { count }
    } nobreak {
      count
    }
  }
}

///|
pub fn grid_spacing(values : Array[Double]) -> GridSpacing {
  if values.length() < 2 {
    GridSpacing::{ count: 0, minimum: 0.0, maximum: 0.0, mean: 0.0 }
  } else {
    let (minimum, maximum, total) = for i = 1, minimum = 1.0E300, maximum = 0.0, total = 0.0; i <
                                       values.length(); {
      let spacing = positive_or_zero(values[i] - values[i - 1])
      continue i + 1,
        if spacing < minimum {
          spacing
        } else {
          minimum
        },
        max_or_zero(maximum, spacing),
        total + spacing
    } nobreak {
      (minimum, maximum, total)
    }
    let count = values.length() - 1
    GridSpacing::{ count, minimum, maximum, mean: total / count.to_double() }
  }
}

///|
pub fn grid_is_well_formed(
  values : Array[Double],
  minimum_spacing~ : Double,
) -> Bool {
  values.length() >= 2 &&
  minimum_spacing >= 0.0 &&
  monotonicity_violations(values, increasing=true) == 0 &&
  grid_spacing(values).minimum >= minimum_spacing
}

///|
pub fn normalized_bias(metrics : ResidualMetrics) -> Double {
  if metrics.count == 0 {
    0.0
  } else {
    metrics.signed_bias / max_or_zero(metrics.max_abs_error, 1.0E-12)
  }
}

///|
pub fn metrics_summary(metrics : ResidualMetrics) -> String {
  "n=" +
  metrics.count.to_string() +
  ", mae=" +
  metrics.mean_absolute_error.to_string() +
  ", rmse=" +
  metrics.rmse.to_string() +
  ", max_abs=" +
  metrics.max_abs_error.to_string() +
  ", max_rel=" +
  metrics.max_relative_error.to_string()
}