///|
pub(all) suberror CodaError {
  Invalid(String)
  Shape(String)
  Budget(String)
  Numerical(String)
} derive(@debug.Debug)

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

///|
fn finite(x : Double) -> Bool {
  x == x && x.abs() <= 1.0e300
}

///|
fn checked(x : Double) -> Double raise CodaError {
  if !finite(x) {
    raise Numerical("non-finite or out-of-range intermediate")
  }
  x
}

///|
fn vector_size(n : Int) -> Unit raise CodaError {
  if n < 2 || n > 64 {
    raise Budget("composition dimension must be 2..64")
  }
}

///|
fn real_vector(x : Array[Double]) -> Unit raise CodaError {
  for v in x {
    if !finite(v) {
      raise Invalid("expected finite numbers bounded by 1e300")
    }
  }
}

///|
/// Normalize nonnegative weights without summing large raw numbers.
/// Zeros are allowed here; log-ratio routines require strictly positive parts.
pub fn closure(x : Array[Double]) -> Array[Double] raise CodaError {
  vector_size(x.length())
  real_vector(x)
  let mut scale = 0.0
  for v in x {
    if v < 0.0 {
      raise Invalid("negative composition part")
    }
    if v > scale {
      scale = v
    }
  }
  if scale == 0.0 {
    raise Invalid("all-zero composition")
  }
  let mut total = 0.0
  for v in x {
    total += v / scale
  }
  let result = x.map(v => checked(v / scale / total))
  for i, v in x {
    if v > 0.0 && result[i] == 0.0 {
      raise Numerical("closure underflow")
    }
  }
  result
}

///|
pub fn positive(x : Array[Double]) -> Unit raise CodaError {
  vector_size(x.length())
  real_vector(x)
  for v in x {
    if v <= 0.0 {
      raise Invalid("log-ratios require strictly positive parts")
    }
  }
}