///|
pub struct Uncertainty {
  nominal : Float
  lower : Float
  upper : Float
  confidence : Float
} derive(Debug, Eq)

///|
pub fn uncertainty(
  nominal : Float,
  lower : Float,
  upper : Float,
  confidence : Float,
) -> Uncertainty {
  { nominal, lower, upper, confidence }
}

///|
pub fn Uncertainty::width(self : Uncertainty) -> Float {
  self.upper - self.lower
}

///|
pub fn Uncertainty::half_width(self : Uncertainty) -> Float {
  self.width() / 2.0
}

///|
pub fn Uncertainty::relative_width(self : Uncertainty) -> Float {
  if self.nominal == 0.0 {
    0.0
  } else {
    self.width().abs() / self.nominal.abs()
  }
}

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

///|
pub fn Uncertainty::is_valid(self : Uncertainty) -> Bool {
  self.lower <= self.nominal &&
  self.nominal <= self.upper &&
  self.confidence >= 0.0 &&
  self.confidence <= 1.0
}

///|
pub fn Uncertainty::expand(self : Uncertainty, factor : Float) -> Uncertainty {
  let center = self.nominal
  let half = self.half_width() * factor.abs()
  { ..self, lower: center - half, upper: center + half }
}

///|
pub fn Uncertainty::shift(self : Uncertainty, delta : Float) -> Uncertainty {
  {
    ..self,
    nominal: self.nominal + delta,
    lower: self.lower + delta,
    upper: self.upper + delta,
  }
}

///|
pub struct SensitivityPoint {
  parameter : String
  change : Float
  response : Float
} derive(Debug, Eq)

///|
pub fn sensitivity_point(
  parameter : String,
  change : Float,
  response : Float,
) -> SensitivityPoint {
  { parameter, change, response }
}

///|
pub fn SensitivityPoint::elasticity(self : SensitivityPoint) -> Float {
  if self.change == 0.0 {
    0.0
  } else {
    self.response / self.change
  }
}

///|
pub struct SensitivityAnalysis {
  baseline : Float
  points : Array[SensitivityPoint]
} derive(Debug, Eq)

///|
pub fn sensitivity_analysis(
  baseline : Float,
  points : Array[SensitivityPoint],
) -> SensitivityAnalysis {
  { baseline, points }
}

///|
pub fn SensitivityAnalysis::count(self : SensitivityAnalysis) -> Int {
  self.points.length()
}

///|
pub fn SensitivityAnalysis::maximum(
  self : SensitivityAnalysis,
) -> SensitivityPoint? {
  if self.points.length() == 0 {
    None
  } else {
    let mut best = self.points[0]
    for point in self.points[1:] {
      if point.response > best.response {
        best = point
      }
    }
    Some(best)
  }
}

///|
pub fn SensitivityAnalysis::minimum(
  self : SensitivityAnalysis,
) -> SensitivityPoint? {
  if self.points.length() == 0 {
    None
  } else {
    let mut best = self.points[0]
    for point in self.points[1:] {
      if point.response < best.response {
        best = point
      }
    }
    Some(best)
  }
}

///|
pub fn SensitivityAnalysis::mean_response(self : SensitivityAnalysis) -> Float {
  summarize(self.points.map(fn(item) { item.response })).mean
}

///|
pub fn SensitivityAnalysis::ranked(
  self : SensitivityAnalysis,
) -> Array[SensitivityPoint] {
  let result = self.points.copy()
  result.sort_by(fn(left, right) {
    if left.response > right.response {
      -1
    } else if left.response < right.response {
      1
    } else {
      0
    }
  })
  result
}

///|
pub fn SensitivityAnalysis::to_table(self : SensitivityAnalysis) -> ReportTable {
  let rows : Array[Array[String]] = []
  for point in self.ranked() {
    rows.push([
      point.parameter,
      "{point.change}",
      "{point.response}",
      "{point.elasticity()}",
    ])
  }
  table(["parameter", "change", "response", "elasticity"], rows)
}

///|
pub struct MonteCarloSummary {
  trials : Int
  mean : Float
  minimum : Float
  maximum : Float
  p05 : Float
  p50 : Float
  p95 : Float
} derive(Debug, Eq)

///|
pub fn monte_carlo_summary(values : Array[Float]) -> MonteCarloSummary {
  let stats = summarize(values)
  {
    trials: stats.count,
    mean: stats.mean,
    minimum: stats.minimum,
    maximum: stats.maximum,
    p05: percentile(values, 5.0).unwrap_or(0.0),
    p50: percentile(values, 50.0).unwrap_or(0.0),
    p95: percentile(values, 95.0).unwrap_or(0.0),
  }
}

///|
pub fn MonteCarloSummary::spread(self : MonteCarloSummary) -> Float {
  self.p95 - self.p05
}

///|
pub fn MonteCarloSummary::is_stable(
  self : MonteCarloSummary,
  maximum_spread : Float,
) -> Bool {
  self.spread() <= maximum_spread
}

///|
pub fn MonteCarloSummary::to_table(self : MonteCarloSummary) -> ReportTable {
  table(["metric", "value"], [
    ["trials", "\{self.trials}"],
    ["mean", "\{self.mean}"],
    ["minimum", "\{self.minimum}"],
    ["maximum", "\{self.maximum}"],
    ["p05", "\{self.p05}"],
    ["p50", "\{self.p50}"],
    ["p95", "\{self.p95}"],
  ])
}

///|
pub fn propagate_add(left : Uncertainty, right : Uncertainty) -> Uncertainty {
  uncertainty(
    left.nominal + right.nominal,
    left.lower + right.lower,
    left.upper + right.upper,
    if left.confidence < right.confidence {
      left.confidence
    } else {
      right.confidence
    },
  )
}

///|
pub fn propagate_subtract(
  left : Uncertainty,
  right : Uncertainty,
) -> Uncertainty {
  uncertainty(
    left.nominal - right.nominal,
    left.lower - right.upper,
    left.upper - right.lower,
    if left.confidence < right.confidence {
      left.confidence
    } else {
      right.confidence
    },
  )
}

///|
pub fn propagate_scale(value : Uncertainty, factor : Float) -> Uncertainty {
  if factor >= 0.0 {
    uncertainty(
      value.nominal * factor,
      value.lower * factor,
      value.upper * factor,
      value.confidence,
    )
  } else {
    uncertainty(
      value.nominal * factor,
      value.upper * factor,
      value.lower * factor,
      value.confidence,
    )
  }
}

///|
pub fn propagate_multiply(
  left : Uncertainty,
  right : Uncertainty,
) -> Uncertainty {
  let values = [
    left.lower * right.lower,
    left.lower * right.upper,
    left.upper * right.lower,
    left.upper * right.upper,
  ]
  let stats = summarize(values)
  uncertainty(
    left.nominal * right.nominal,
    stats.minimum,
    stats.maximum,
    if left.confidence < right.confidence {
      left.confidence
    } else {
      right.confidence
    },
  )
}

///|
pub fn propagate_divide(left : Uncertainty, right : Uncertainty) -> Uncertainty {
  if right.contains(0.0) {
    uncertainty(0.0, -1.0e30, 1.0e30, 0.0)
  } else {
    propagate_multiply(
      left,
      uncertainty(
        1.0 / right.nominal,
        1.0 / right.upper,
        1.0 / right.lower,
        right.confidence,
      ),
    )
  }
}

///|
pub fn uncertainty_table(values : Array[Uncertainty]) -> ReportTable {
  let rows : Array[Array[String]] = []
  for value in values {
    rows.push([
      "\{value.nominal}",
      "\{value.lower}",
      "\{value.upper}",
      "\{value.confidence}",
      "\{value.relative_width()}",
    ])
  }
  table(["nominal", "lower", "upper", "confidence", "relative width"], rows)
}

///|
pub fn uncertainty_quality_gate(values : Array[Uncertainty]) -> Bool {
  for value in values {
    if !value.is_valid() {
      return false
    }
  }
  true
}