///|
pub(all) struct AggregateBin {
  index : Int
  lower : Double
  upper : Double
  count : Int
  mean : Double
  minimum : Double
  maximum : Double
  standard_deviation : Double
} derive(Debug, Eq, ToJson)

///|
pub(all) struct GroupSummary {
  key : String
  count : Int
  mean : Double
  weighted_mean : Double
  minimum : Double
  maximum : Double
  total : Double
} derive(Debug, Eq, ToJson)

///|
pub(all) struct Histogram {
  bins : Array[AggregateBin]
  total_count : Int
  underflow : Int
  overflow : Int
} derive(Debug, Eq, ToJson)

///|
pub fn bin_index(
  value : Double,
  lower : Double,
  upper : Double,
  bins : Int,
) -> Int {
  if bins < 1 || upper <= lower {
    return -1
  }
  if value < lower {
    -1
  } else if value >= upper {
    bins
  } else {
    ((value - lower) / (upper - lower) * bins.to_double())
    .to_int()
    .min(bins - 1)
  }
}

///|
pub fn histogram(
  values : ArrayView[Double],
  lower : Double,
  upper : Double,
  bins : Int,
) -> Histogram raise @domain.ExtractionError {
  if values.length() == 0 || bins < 1 || upper <= lower {
    raise @domain.ExtractionError::InvalidArgument(
      message="histogram inputs are invalid",
    )
  }
  let counts = Array::make(bins, 0)
  let totals = Array::make(bins, 0.0)
  let squares = Array::make(bins, 0.0)
  let minimums = Array::make(bins, 1.0e30)
  let maximums = Array::make(bins, -1.0e30)
  let mut underflow = 0
  let mut overflow = 0
  for value in values {
    let index = bin_index(value, lower, upper, bins)
    if index < 0 {
      underflow += 1
    } else if index >= bins {
      overflow += 1
    } else {
      counts[index] += 1
      totals[index] += value
      squares[index] += value * value
      minimums[index] = minimums[index].min(value)
      maximums[index] = maximums[index].max(value)
    }
  }
  let result = []
  let width = (upper - lower) / bins.to_double()
  for index in 0.. Array[Double] {
  let densities = []
  for bin in histogram.bins {
    let width = bin.upper - bin.lower
    densities.push(
      if width == 0.0 {
        0.0
      } else {
        bin.count.to_double() / histogram.total_count.to_double() / width
      },
    )
  }
  densities
}

///|
pub fn histogram_quantile(
  histogram : Histogram,
  probability : Double,
) -> Double raise @domain.ExtractionError {
  if histogram.total_count == 0 || probability < 0.0 || probability > 1.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="histogram quantile is invalid",
    )
  }
  let target = probability * histogram.total_count.to_double()
  let mut cumulative = histogram.underflow.to_double()
  for bin in histogram.bins {
    cumulative += bin.count.to_double()
    if cumulative >= target {
      return (bin.lower + bin.upper) / 2.0
    }
  }
  histogram.bins[histogram.bins.length() - 1].upper
}

///|
pub fn cumulative_counts(histogram : Histogram) -> Array[Int] {
  let result = []
  let mut cumulative = histogram.underflow
  for bin in histogram.bins {
    cumulative += bin.count
    result.push(cumulative)
  }
  result
}

///|
pub fn group_by_label(
  labels : ArrayView[String],
  values : ArrayView[Double],
) -> Array[GroupSummary] raise @domain.ExtractionError {
  if labels.length() != values.length() || labels.length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="group arrays are invalid",
    )
  }
  let keys = []
  let totals = []
  let counts = []
  let minimums = []
  let maximums = []
  for index in 0.. Array[GroupSummary] raise @domain.ExtractionError {
  if labels.length() != values.length() ||
    labels.length() != weights.length() ||
    labels.length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="weighted group arrays are invalid",
    )
  }
  let keys = []
  let totals = []
  let weighted_totals = []
  let weights_total = []
  let counts = []
  let minimums = []
  let maximums = []
  for index in 0.. String {
  let lines = ["key,count,mean,weighted_mean,minimum,maximum,total"]
  for group in groups {
    lines.push(
      group.key +
      "," +
      group.count.to_string() +
      "," +
      group.mean.to_string() +
      "," +
      group.weighted_mean.to_string() +
      "," +
      group.minimum.to_string() +
      "," +
      group.maximum.to_string() +
      "," +
      group.total.to_string(),
    )
  }
  lines.join("\n")
}

///|
pub fn group_summary_markdown(groups : ArrayView[GroupSummary]) -> String {
  let lines = [
    "| Group | Count | Mean | Weighted mean | Minimum | Maximum | Total |", "|---|---:|---:|---:|---:|---:|---:|",
  ]
  for group in groups {
    lines.push(
      "| " +
      group.key +
      " | " +
      group.count.to_string() +
      " | " +
      group.mean.to_string() +
      " | " +
      group.weighted_mean.to_string() +
      " | " +
      group.minimum.to_string() +
      " | " +
      group.maximum.to_string() +
      " | " +
      group.total.to_string() +
      " |",
    )
  }
  lines.join("\n")
}

///|
pub fn aggregate_mean(values : ArrayView[Double]) -> Double {
  if values.length() == 0 {
    0.0
  } else {
    values.fold(init=0.0, (sum, value) => sum + value) /
    values.length().to_double()
  }
}

///|
pub fn aggregate_minimum(values : ArrayView[Double]) -> Double {
  if values.length() == 0 {
    0.0
  } else {
    let mut result = values[0]
    for value in values {
      result = result.min(value)
    }
    result
  }
}

///|
pub fn aggregate_maximum(values : ArrayView[Double]) -> Double {
  if values.length() == 0 {
    0.0
  } else {
    let mut result = values[0]
    for value in values {
      result = result.max(value)
    }
    result
  }
}

///|
pub fn aggregate_range(values : ArrayView[Double]) -> (Double, Double) {
  (aggregate_minimum(values), aggregate_maximum(values))
}

///|
pub fn aggregate_variance(values : ArrayView[Double]) -> Double {
  if values.length() == 0 {
    0.0
  } else {
    let mean = aggregate_mean(values)
    let mut total = 0.0
    for value in values {
      let difference = value - mean
      total += difference * difference
    }
    total / values.length().to_double()
  }
}

///|
pub fn aggregate_standard_deviation(values : ArrayView[Double]) -> Double {
  aggregate_variance(values).sqrt()
}

///|
pub fn aggregate_median(values : ArrayView[Double]) -> Double {
  if values.length() == 0 {
    0.0
  } else {
    let ordered = values.to_owned()
    ordered.sort()
    let middle = ordered.length() / 2
    if ordered.length() % 2 == 0 {
      (ordered[middle - 1] + ordered[middle]) / 2.0
    } else {
      ordered[middle]
    }
  }
}

///|
pub fn aggregate_mad(values : ArrayView[Double]) -> Double {
  let median = aggregate_median(values)
  let deviations = [ for value in values => (value - median).abs() ]
  aggregate_median(deviations)
}

///|
pub fn aggregate_cv(values : ArrayView[Double]) -> Double {
  let mean = aggregate_mean(values)
  if mean == 0.0 {
    0.0
  } else {
    aggregate_standard_deviation(values) / mean.abs()
  }
}

///|
pub fn aggregate_z_scores(values : ArrayView[Double]) -> Array[Double] {
  let mean = aggregate_mean(values)
  let deviation = aggregate_standard_deviation(values)
  if deviation == 0.0 {
    Array::make(values.length(), 0.0)
  } else {
    [
      for value in values => (value - mean) / deviation
    ]
  }
}

///|
pub fn aggregate_trimmed_mean(
  values : ArrayView[Double],
  trim_fraction : Double,
) -> Double raise @domain.ExtractionError {
  if trim_fraction < 0.0 || trim_fraction >= 0.5 {
    raise @domain.ExtractionError::InvalidFraction(
      name="trim fraction",
      value=trim_fraction,
    )
  }
  let ordered = values.to_owned()
  ordered.sort()
  let trim = (ordered.length().to_double() * trim_fraction).to_int()
  if ordered.length() <= 2 * trim {
    0.0
  } else {
    let mut total = 0.0
    for index in trim..<(ordered.length() - trim) {
      total += ordered[index]
    }
    total / (ordered.length() - 2 * trim).to_double()
  }
}

///|
pub fn aggregate_winsorized_mean(
  values : ArrayView[Double],
  fraction : Double,
) -> Double raise @domain.ExtractionError {
  if fraction < 0.0 || fraction >= 0.5 || values.length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="winsorization inputs are invalid",
    )
  }
  let ordered = values.to_owned()
  ordered.sort()
  let count = (ordered.length().to_double() * fraction).to_int()
  for index in 0.. Double raise @domain.ExtractionError {
  if first.length() != second.length() || first.length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="correlation arrays are invalid",
    )
  }
  let first_mean = aggregate_mean(first)
  let second_mean = aggregate_mean(second)
  let mut numerator = 0.0
  let mut first_variance = 0.0
  let mut second_variance = 0.0
  for index in 0.. Double raise @domain.ExtractionError {
  if first.length() != second.length() || first.length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="covariance arrays are invalid",
    )
  }
  let first_mean = aggregate_mean(first)
  let second_mean = aggregate_mean(second)
  let mut total = 0.0
  for index in 0.. Array[Int] {
  let result = Array::make(values.length(), 1)
  for index in 0.. values[index] {
        result[index] += 1
      }
    }
  }
  result
}

///|
pub fn aggregate_sort_indices(values : ArrayView[Double]) -> Array[Int] {
  let indices = [ for index in 0.. index ]
  for first in 0..