///|
/// Summary statistics for one box-and-whisker series.
pub struct BoxStats {
  lower_whisker : Double
  q1 : Double
  median : Double
  q3 : Double
  upper_whisker : Double
  fliers : Array[Double]
}

///|
fn finite(value : Double) -> Bool {
  !value.is_nan() && !value.is_inf()
}

///|
fn sorted_finite(values : Array[Double]) -> Array[Double] {
  let mut result : Array[Double] = []
  for value in values {
    if finite(value) {
      let mut inserted = false
      let next = Array(capacity=result.length() + 1)
      for current in result {
        if !inserted && value < current {
          next.push(value)
          inserted = true
        }
        next.push(current)
      }
      if !inserted {
        next.push(value)
      }
      result = next
    }
  }
  result
}

///|
fn percentile(values : Array[Double], fraction : Double) -> Double {
  let position = fraction * Double::from_int(values.length() - 1)
  let lower = position.floor().to_int()
  let upper = position.ceil().to_int()
  if lower == upper {
    values[lower]
  } else {
    let weight = position - Double::from_int(lower)
    values[lower] + (values[upper] - values[lower]) * weight
  }
}

///|
/// Compute Tukey box-and-whisker statistics using a 1.5 IQR whisker rule.
pub fn box_stats(values : Array[Double]) -> BoxStats raise @core.DataError {
  let sorted = sorted_finite(values)
  if sorted.length() == 0 {
    raise @core.DataError::InvalidBoxPlot(
      message="boxplot requires at least one finite value",
    )
  }
  let q1 = percentile(sorted, 0.25)
  let median = percentile(sorted, 0.5)
  let q3 = percentile(sorted, 0.75)
  let iqr = q3 - q1
  let lower_fence = q1 - 1.5 * iqr
  let upper_fence = q3 + 1.5 * iqr
  let mut lower_whisker = sorted[0]
  let mut upper_whisker = sorted[sorted.length() - 1]
  let mut found_lower = false
  let fliers : Array[Double] = []
  for value in sorted {
    if value >= lower_fence && !found_lower {
      lower_whisker = value
      found_lower = true
    }
    if value <= upper_fence {
      upper_whisker = value
    } else {
      fliers.push(value)
    }
  }
  for value in sorted {
    if value < lower_fence {
      fliers.push(value)
    }
  }
  { lower_whisker, q1, median, q3, upper_whisker, fliers, }
}

///|
pub fn BoxStats::lower_whisker(self : BoxStats) -> Double {
  self.lower_whisker
}

///|
pub fn BoxStats::q1(self : BoxStats) -> Double {
  self.q1
}

///|
pub fn BoxStats::median(self : BoxStats) -> Double {
  self.median
}

///|
pub fn BoxStats::q3(self : BoxStats) -> Double {
  self.q3
}

///|
pub fn BoxStats::upper_whisker(self : BoxStats) -> Double {
  self.upper_whisker
}

///|
pub fn BoxStats::fliers(self : BoxStats) -> Array[Double] {
  self.fliers.copy()
}