///|
fn sorted_values(values : Array[Double]) -> Array[Double] {
  let result = values.copy()
  result.sort_by(Double::compare)
  result
}

///|
pub fn quantile(values : Array[Double], fraction : Double) -> Double {
  if values.is_empty() {
    return 0.0
  }
  let sorted = sorted_values(values)
  let position = Double::min(Double::max(fraction, 0.0), 1.0) *
    Double::from_int(sorted.length() - 1)
  let lower = position.to_int()
  let upper = Int::min(lower + 1, sorted.length() - 1)
  let weight = position - Double::from_int(lower)
  sorted[lower] * (1.0 - weight) + sorted[upper] * weight
}

///|
pub fn summarize_values(values : Array[Double]) -> MetricDistribution {
  if values.is_empty() {
    return {
      count: 0,
      mean: 0.0,
      min: 0.0,
      max: 0.0,
      median: 0.0,
      p25: 0.0,
      p75: 0.0,
      stddev: 0.0,
    }
  }
  let mut total = 0.0
  let mut minimum = values[0]
  let mut maximum = values[0]
  for value in values {
    total += value
    minimum = Double::min(minimum, value)
    maximum = Double::max(maximum, value)
  }
  let mean = total / Double::from_int(values.length())
  let mut squared = 0.0
  for value in values {
    let delta = value - mean
    squared += delta * delta
  }
  {
    count: values.length(),
    mean,
    min: minimum,
    max: maximum,
    median: quantile(values, 0.5),
    p25: quantile(values, 0.25),
    p75: quantile(values, 0.75),
    stddev: Double::sqrt(squared / Double::from_int(values.length())),
  }
}

///|
pub fn summarize_report_metric(
  report : BenchmarkReport,
  name : String,
) -> MetricDistribution {
  let values : Array[Double] = []
  for query in report.queries {
    match query.metrics.get(name) {
      Some(value) => values.push(value)
      None => ()
    }
  }
  summarize_values(values)
}

///|
pub fn trimmed_mean(values : Array[Double], trim_fraction : Double) -> Double {
  if values.is_empty() {
    return 0.0
  }
  let sorted = sorted_values(values)
  let trim = Int::max(
    0,
    Int::min(
      sorted.length() / 2,
      (Double::from_int(sorted.length()) * Double::max(trim_fraction, 0.0)).to_int(),
    ),
  )
  let start = trim
  let end = sorted.length() - trim
  if start >= end {
    return 0.0
  }
  let mut total = 0.0
  for index in start.. Double {
  let limit = Int::min(values.length(), weights.length())
  let mut total = 0.0
  let mut weight_total = 0.0
  for index in 0.. Double {
  summarize_report_metric(report, metric_name(prefix, cutoff)).mean
}

///|
pub fn metric_wins(
  baseline : BenchmarkReport,
  candidate : BenchmarkReport,
  name : String,
) -> (Int, Int, Int) {
  let base : Map[String, Double] = Map([])
  let next : Map[String, Double] = Map([])
  for query in baseline.queries {
    match query.metrics.get(name) {
      Some(value) => base[query.query_id] = value
      None => ()
    }
  }
  for query in candidate.queries {
    match query.metrics.get(name) {
      Some(value) => next[query.query_id] = value
      None => ()
    }
  }
  let mut wins = 0
  let mut losses = 0
  let mut ties = 0
  let ids : Map[String, Unit] = Map([])
  for id, _ in base {
    ids[id] = ()
  }
  for id, _ in next {
    ids[id] = ()
  }
  for id, _ in ids {
    let left = base.get_or_default(id, 0.0)
    let right = next.get_or_default(id, 0.0)
    if right > left + 0.000000001 {
      wins += 1
    } else if right < left - 0.000000001 {
      losses += 1
    } else {
      ties += 1
    }
  }
  (wins, losses, ties)
}