///|
/// Owned empirical distribution on the unit square. Construction copies input.
pub struct EmpiricalCopula {
  priv points : Array[Pair]
}

///|
/// Already ranked uniforms; use from_raw for finite raw measurements.
pub fn EmpiricalCopula::new(
  data : Array[Pair],
) -> EmpiricalCopula raise CopulaError {
  validate_uniforms(data, 2)
  { points: data.copy(), }
}

///|
pub fn EmpiricalCopula::from_raw(
  data : Array[Pair],
  policy~ : TiePolicy = Midrank,
) -> EmpiricalCopula raise CopulaError {
  EmpiricalCopula::new(pseudo_observations(data, policy~))
}

///|
pub fn EmpiricalCopula::size(self : EmpiricalCopula) -> Int32 {
  self.points.length()
}

///|
/// Return a defensive copy of pseudo-observations.
pub fn EmpiricalCopula::observations(self : EmpiricalCopula) -> Array[Pair] {
  self.points.copy()
}

///|
/// Empirical joint CDF, both coordinates use <=. Margins are discrete.
pub fn EmpiricalCopula::cdf(
  self : EmpiricalCopula,
  u : Double,
  v : Double,
) -> Double raise CopulaError {
  probability(u, "u")
  probability(v, "v")
  let mut count = 0
  for p in self.points {
    if p.x <= u && p.y <= v {
      count = count + 1
    }
  }
  count.to_double() / self.points.length().to_double()
}

///|
/// Probability of (lo_u,hi_u] x (lo_v,hi_v], including exact discrete ties.
pub fn EmpiricalCopula::rectangle(
  self : EmpiricalCopula,
  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")
  }
  let mut count = 0
  for p in self.points {
    if p.x > lo_u && p.x <= hi_u && p.y > lo_v && p.y <= hi_v {
      count = count + 1
    }
  }
  count.to_double() / self.points.length().to_double()
}

///|
/// Smoothed checkerboard CDF uses ordinal (untied) ranks only.
/// Each rank cell spreads mass uniformly over its n-by-n grid cell.
pub fn checkerboard_cdf(
  raw : Array[Pair],
  u : Double,
  v : Double,
) -> Double raise CopulaError {
  probability(u, "u")
  probability(v, "v")
  let data = pseudo_observations(raw, policy=RejectTies)
  let n = data.length().to_double()
  let mut total = 0.0
  for point in data {
    let rx = point.x * (n + 1.0)
    let ry = point.y * (n + 1.0)
    let fx = (n * u - rx + 1.0).max(0.0).min(1.0)
    let fy = (n * v - ry + 1.0).max(0.0).min(1.0)
    total = total + fx * fy
  }
  checked_probability(total / n)
}