///|
/// Joint event decomposition for uniform thresholds, evaluated in one model.
pub struct JointRisk {
  lower_both : Double
  upper_both : Double
  upper_either : Double
  x_above_y_below : Double
  x_below_y_above : Double
} derive(Debug)

///|
pub extend JointRisk with @debug.Debug::{to_repr}

///|
pub fn Copula::risk(
  self : Copula,
  u : Double,
  v : Double,
) -> JointRisk raise CopulaError {
  let c = self.cdf(u, v)
  {
    lower_both: c,
    upper_both: checked_probability(1.0 - u - v + c),
    upper_either: checked_probability(1.0 - c),
    x_above_y_below: checked_probability(v - c),
    x_below_y_above: checked_probability(u - c),
  }
}

///|
/// Continuous rectangle mass using inclusion-exclusion, with explicit order.
pub fn Copula::rectangle(
  self : Copula,
  lo_u : Double,
  hi_u : Double,
  lo_v : Double,
  hi_v : Double,
) -> Double raise CopulaError {
  probability(lo_u, "lo_u")
  probability(hi_u, "hi_u")
  probability(lo_v, "lo_v")
  probability(hi_v, "hi_v")
  if lo_u > hi_u || lo_v > hi_v {
    raise InvalidInput("reversed rectangle")
  }
  checked_probability(
    self.cdf(hi_u, hi_v) -
    self.cdf(lo_u, hi_v) -
    self.cdf(hi_u, lo_v) +
    self.cdf(lo_u, lo_v),
  )
}

///|
/// Expected waiting time under independent, identically distributed event periods.
/// Zero probability gives None, never an artificial finite return period.
pub fn return_period(
  event_probability : Double,
  period_length : Double,
) -> Double? raise CopulaError {
  probability(event_probability, "event probability")
  finite(period_length, "period length")
  if period_length <= 0.0 {
    raise InvalidInput("period length must be positive")
  }
  if event_probability == 0.0 {
    return None
  }
  let result = period_length / event_probability
  if result.is_inf() {
    raise NumericalFailure("return period overflow")
  }
  Some(result)
}

///|
/// P(V>v | U>u), a threshold event; differs from conditional(v|u) at a point.
pub fn Copula::conditional_exceedance(
  self : Copula,
  u : Double,
  v : Double,
) -> Double raise CopulaError {
  probability(u, "u")
  probability(v, "v")
  if u == 1.0 {
    raise InvalidInput("conditioning event has zero probability")
  }
  checked_probability(self.risk(u, v).upper_both / (1.0 - u))
}

///|
/// Theoretical asymptotic same-tail coefficients; no inference from data.
pub fn Copula::tail_coefficients(self : Copula) -> (Double, Double) {
  let (lower, upper) = match self.family {
    Independence => (0.0, 0.0)
    Clayton => (@math.exp(-@math.ln(2.0) / self.theta), 0.0)
    Gumbel => (0.0, 2.0 - @math.exp(@math.ln(2.0) / self.theta))
    Frank => (0.0, 0.0)
  }
  match self.rotation {
    R0 => (lower, upper)
    R180 => (upper, lower)
    R90 | R270 => (0.0, 0.0)
  }
}