///|
pub(all) struct BlockExplanation {
  start : Int
  end : Int
  fitted : Double
  observed_mean : Double
  observed_min : Double
  observed_max : Double
  weight : Double
  squared_error : Double
  absolute_error : Double
} derive(Debug)

///|
pub fn explain_fit(
  y : Array[Double],
  weights : Array[Double],
  fit : Fit,
) -> Result[Array[BlockExplanation], String] {
  match validate_series(y, weights) {
    Err(e) => return Err(e)
    Ok(_) => ()
  }
  let expanded = match expand_blocks(fit.blocks, y.length()) {
    Err(e) => return Err(e)
    Ok(v) => v
  }
  if expanded != fit.values {
    return Err("fit partition mismatch")
  }
  let explanations : Array[BlockExplanation] = []
  for block in fit.blocks {
    let mut weight = 0.0
    let mut observed_mean = 0.0
    let mut observed_min = y[block.start]
    let mut observed_max = y[block.start]
    let mut squared_error = 0.0
    let mut absolute_error = 0.0
    for i = block.start; i < block.end; i = i + 1 {
      let next_weight = weight + weights[i]
      observed_mean = observed_mean * (weight / next_weight) +
        y[i] * (weights[i] / next_weight)
      weight = next_weight
      if y[i] < observed_min {
        observed_min = y[i]
      }
      if y[i] > observed_max {
        observed_max = y[i]
      }
      let residual = block.value - y[i]
      squared_error = squared_error + weights[i] * residual * residual
      absolute_error = absolute_error + weights[i] * residual.abs()
    }
    if !finite(squared_error) ||
      !finite(absolute_error) ||
      !finite(observed_mean) {
      return Err("explanation arithmetic overflow")
    }
    if (weight - block.weight).abs() > 1.0e-12 * weight {
      return Err("block weight mismatch")
    }
    explanations.push({
      start: block.start,
      end: block.end,
      fitted: block.value,
      observed_mean,
      observed_min,
      observed_max,
      weight,
      squared_error,
      absolute_error,
    })
  }
  Ok(explanations)
}

///|
pub fn explanation_table(explanations : Array[BlockExplanation]) -> String {
  let lines = [
    "start\tend\tfitted\tmean\tmin\tmax\tweight\tsquared_error\tabsolute_error",
  ]
  for e in explanations {
    lines.push(
      e.start.to_string() +
      "\t" +
      e.end.to_string() +
      "\t" +
      e.fitted.to_string() +
      "\t" +
      e.observed_mean.to_string() +
      "\t" +
      e.observed_min.to_string() +
      "\t" +
      e.observed_max.to_string() +
      "\t" +
      e.weight.to_string() +
      "\t" +
      e.squared_error.to_string() +
      "\t" +
      e.absolute_error.to_string(),
    )
  }
  lines.join("\n") + "\n"
}

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