///|
/// A prediction set on the real line. lower > upper is a valid empty set.
pub(all) struct Interval {
  lower : Double
  upper : Double
}

///|
pub fn Interval::contains(self : Interval, value : Double) -> Bool {
  finite(value) && self.lower <= value && value <= self.upper
}

///|
pub fn Interval::is_empty(self : Interval) -> Bool {
  self.lower > self.upper
}

///|
pub fn Interval::width(self : Interval) -> Double {
  if self.is_empty() {
    0.0
  } else {
    self.upper - self.lower
  }
}

///|
pub fn Interval::intersect(self : Interval, other : Interval) -> Interval {
  {
    lower: if self.lower > other.lower {
      self.lower
    } else {
      other.lower
    },
    upper: if self.upper < other.upper {
      self.upper
    } else {
      other.upper
    },
  }
}

///|
fn same_length(a : Int32, b : Int32) -> Unit raise ConformalError {
  if a != b {
    raise Invalid("array lengths differ")
  }
}

///|
fn checked_interval(
  center : Double,
  radius : Double,
) -> Interval raise ConformalError {
  if !finite(center) {
    raise Invalid("prediction must be finite")
  }
  if radius.is_pos_inf() {
    return { lower: @double.neg_infinity, upper: @double.infinity, }
  }
  let lower = center - radius
  let upper = center + radius
  if !finite(lower) || !finite(upper) {
    raise Invalid("finite interval endpoint overflow")
  }
  { lower, upper, }
}

///|
/// Absolute-residual split conformal calibration, independent of model training.
pub fn absolute_scores(
  truth : Array[Double],
  prediction : Array[Double],
) -> Scores raise ConformalError {
  validate_values(truth, "truth")
  validate_values(prediction, "prediction")
  same_length(truth.length(), prediction.length())
  let residuals = []
  for i in 0.. Array[Interval] raise ConformalError {
  validate_values(prediction, "prediction")
  let radius = scores.threshold(alpha)
  if scores.sorted[0] < 0.0 {
    raise Invalid("absolute residual scores must be nonnegative")
  }
  prediction.map(p => checked_interval(p, radius))
}