///|
pub(all) struct GridReduction {
  source_cells : Int
  target_cells : Int
  source_length : Double
  values : Array[Double]
} derive(Debug, ToJson)

///|
pub fn reduce_average(
  source : Grid1D,
  values : ArrayView[Double],
  target_cells : Int,
) -> GridReduction {
  let count = target_cells.max(1)
  let target = zeros(count)
  let ratio = source.cells.to_double() / count.to_double()
  for i in 0.. Array[Double] {
  resample_periodic(source, values, target, Linear)
}

///|
pub fn reduction_error(
  source : Grid1D,
  values : ArrayView[Double],
  reduction : GridReduction,
) -> Double {
  let reconstructed = prolongate_linear(
    Grid1D::new(reduction.target_cells, source.length),
    reduction.values,
    source,
  )
  field_l2_error(values, reconstructed)
}

///|
pub fn reduction_integral(_grid : Grid1D, reduction : GridReduction) -> Double {
  integrate_trapezoid(
    Grid1D::new(reduction.target_cells, reduction.source_length),
    reduction.values,
  )
}

///|
pub fn reduction_minimum(reduction : GridReduction) -> Double {
  min_value(reduction.values, 0.0)
}

///|
pub fn reduction_maximum(reduction : GridReduction) -> Double {
  max_value(reduction.values, 0.0)
}

///|
pub fn reduction_summary(reduction : GridReduction) -> String {
  "source_cells=\{reduction.source_cells}\ntarget_cells=\{reduction.target_cells}\nminimum=\{reduction_minimum(reduction)}\nmaximum=\{reduction_maximum(reduction)}\n"
}

///|
pub fn reduction_series(
  source : Grid1D,
  values : ArrayView[Double],
  resolutions : ArrayView[Int],
) -> Array[GridReduction] {
  resolutions.map(fn(resolution) { reduce_average(source, values, resolution) })
}

///|
pub fn reduction_errors(
  source : Grid1D,
  values : ArrayView[Double],
  reductions : ArrayView[GridReduction],
) -> Array[Double] {
  reductions.map(fn(reduction) { reduction_error(source, values, reduction) })
}

///|
pub fn reduction_to_csv(reduction : GridReduction) -> String {
  let output = StringBuilder()
  output.write_string("index,value\n")
  for i in 0.. Array[Double] {
  if reductions.length() == 0 {
    []
  } else {
    let width = reductions[0].values.length()
    Array::makei(width, fn(i) {
      mean(reductions.map(fn(reduction) { reduction.values[i] }))
    })
  }
}

///|
pub fn reduction_converges(
  errors : ArrayView[Double],
  tolerance : Double,
) -> Bool {
  errors.length() > 0 && errors[errors.length() - 1] <= tolerance
}