///|
/// Return a quantile using linear interpolation between adjacent order statistics.
/// `probability` is in the closed interval `[0, 1]`.
pub fn quantile(data : Array[Double], probability : Double) -> Double {
  if data.length() == 0 {
    return 0.0
  }
  validate_probability(probability)
  let sorted = copy_and_sort(data)
  if sorted.length() == 1 {
    return sorted[0]
  }
  let position = probability * (sorted.length() - 1).to_double()
  let lower = position.to_int()
  let upper = if lower + 1 < sorted.length() { lower + 1 } else { lower }
  let fraction = position - lower.to_double()
  sorted[lower] + fraction * (sorted[upper] - sorted[lower])
}

///|
pub fn percentile(data : Array[Double], percent : Double) -> Double {
  if percent < 0.0 || percent > 100.0 {
    abort("percent must be in [0, 100]")
  }
  quantile(data, percent / 100.0)
}

///|
pub fn lower_quartile(data : Array[Double]) -> Double {
  quantile(data, 0.25)
}

///|
pub fn upper_quartile(data : Array[Double]) -> Double {
  quantile(data, 0.75)
}

///|
pub fn interquartile_range(data : Array[Double]) -> Double {
  upper_quartile(data) - lower_quartile(data)
}

///|
pub fn quantile_range(
  data : Array[Double],
  lower_probability : Double,
  upper_probability : Double,
) -> Double {
  if lower_probability > upper_probability {
    abort("lower probability must not exceed upper probability")
  }
  quantile(data, upper_probability) - quantile(data, lower_probability)
}

///|
/// The nearest-rank quantile is useful when a discrete order statistic is required.
pub fn nearest_rank(data : Array[Double], probability : Double) -> Double {
  if data.length() == 0 {
    return 0.0
  }
  validate_probability(probability)
  let sorted = copy_and_sort(data)
  let mut rank = (probability * sorted.length().to_double()).to_int()
  if probability > 0.0 &&
    rank.to_double() < probability * sorted.length().to_double() {
    rank += 1
  }
  if rank < 1 {
    rank = 1
  }
  sorted[rank - 1]
}

///|
pub fn median_low(data : Array[Double]) -> Double {
  if data.length() == 0 {
    0.0
  } else {
    let sorted = copy_and_sort(data)
    sorted[(sorted.length() - 1) / 2]
  }
}

///|
pub fn median_high(data : Array[Double]) -> Double {
  if data.length() == 0 {
    0.0
  } else {
    let sorted = copy_and_sort(data)
    sorted[sorted.length() / 2]
  }
}

///|
pub fn weighted_quantile(
  data : Array[Double],
  weights : Array[Double],
  probability : Double,
) -> Double {
  if data.length() == 0 || data.length() != weights.length() {
    return 0.0
  }
  validate_probability(probability)
  let pairs = []
  for index = 0; index < data.length(); index = index + 1 {
    if weights[index] < 0.0 {
      abort("weights must be non-negative")
    }
    pairs.push((data[index], weights[index]))
  }
  pairs.sort()
  let mut total_weight = 0.0
  for pair in pairs {
    total_weight += pair.1
  }
  if total_weight == 0.0 {
    return 0.0
  }
  let target = probability * total_weight
  let mut cumulative = 0.0
  for pair in pairs {
    cumulative += pair.1
    if cumulative >= target {
      return pair.0
    }
  }
  pairs[pairs.length() - 1].0
}

///|
pub fn empirical_cdf(data : Array[Double], value : Double) -> Double {
  if data.length() == 0 {
    0.0
  } else {
    let mut count = 0
    for item in data {
      if item <= value {
        count += 1
      }
    }
    count.to_double() / data.length().to_double()
  }
}

///|
pub fn rank_of(data : Array[Double], value : Double) -> Double {
  if data.length() == 0 {
    0.0
  } else {
    let mut less = 0
    let mut equal = 0
    for item in data {
      if item < value {
        less += 1
      } else if item == value {
        equal += 1
      }
    }
    less.to_double() + (equal.to_double() + 1.0) / 2.0
  }
}

///|
pub fn ranks(data : Array[Double]) -> Array[Double] {
  let result = []
  for value in data {
    result.push(rank_of(data, value))
  }
  result
}

///|
pub fn quantile_bins(data : Array[Double], bins : Int) -> Array[Int] {
  if bins <= 0 {
    abort("bins must be positive")
  }
  let result = []
  for value in data {
    let mut bucket = (empirical_cdf(data, value) * bins.to_double()).to_int()
    if bucket >= bins {
      bucket = bins - 1
    }
    result.push(bucket)
  }
  result
}

///|
pub fn order_statistics(
  data : Array[Double],
  positions : Array[Int],
) -> Array[Double] {
  let sorted = copy_and_sort(data)
  let result = []
  for position in positions {
    if position < 0 || position >= sorted.length() {
      abort("order statistic position out of bounds")
    }
    result.push(sorted[position])
  }
  result
}

///|
pub fn five_number_summary(data : Array[Double]) -> Array[Double] {
  if data.length() == 0 {
    []
  } else {
    [
      min_value(data),
      lower_quartile(data),
      median(data),
      upper_quartile(data),
      max_value(data),
    ]
  }
}