///|
/// Sparse canonical mass assignment; empty-set mass is retained explicitly.
pub struct Mass {
  priv domain : Frame
  priv focals : Array[(Int32, Double)]
} derive(Debug, Eq)

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

///|
pub extend Mass with Eq::{equal, not_equal}

///|
fn finite(x : Double) -> Bool {
  !x.is_nan() && x != 1.0 / 0.0 && x != -1.0 / 0.0
}

///|
fn magnitude(x : Double) -> Double {
  if x < 0.0 {
    -x
  } else {
    x
  }
}

///|
fn stable_sum(values : Array[Double]) -> Double {
  let mut sum = 0.0
  let mut correction = 0.0
  for value in values {
    let y = value - correction
    let next = sum + y
    correction = next - sum - y
    sum = next
  }
  sum
}

///|
/// Duplicate subsets are merged; totals must be one within 1e-10.
/// Tiny positive focals are preserved, not pruned by a hidden threshold.
pub fn mass(
  f : Frame,
  entries : Array[(Int32, Double)],
  budget~ : Limits = default_limits(),
) -> Mass raise BeliefError {
  budget.check_dense(f)
  if entries.length() == 0 {
    raise Invalid("mass requires at least one entry")
  }
  if entries.length() > budget.focals {
    raise Budget("input focal budget exceeded")
  }
  let bins = Array::make(f.universe() + 1, 0.0)
  let corrections = Array::make(bins.length(), 0.0)
  for (mask, value) in entries {
    f.check_mask(mask)
    if !finite(value) || value < 0.0 || value > 1.0 {
      raise Invalid("mass weight must be finite and within [0,1]")
    }
    let y = value - corrections[mask]
    let next = bins[mask] + y
    corrections[mask] = next - bins[mask] - y
    bins[mask] = next
  }
  let total = stable_sum(bins)
  if magnitude(total - 1.0) > 1.0e-10 {
    raise Invalid("mass weights must sum to one")
  }
  from_bins(f, bins, budget)
}

///|
fn from_bins(
  f : Frame,
  bins : Array[Double],
  budget : Limits,
) -> Mass raise BeliefError {
  budget.check_dense(f)
  if bins.length() != f.universe() + 1 {
    raise Invalid("dense mass shape mismatch")
  }
  let total = stable_sum(bins)
  if !finite(total) || total <= 0.0 {
    raise Invalid("mass has no finite positive total")
  }
  let result : Array[(Int32, Double)] = []
  for mask = 0; mask < bins.length(); mask = mask + 1 {
    let value = bins[mask]
    if !finite(value) || value < 0.0 {
      raise Invalid("invalid computed mass weight")
    }
    if value > 0.0 {
      if result.length() >= budget.focals {
        raise Budget("output focal budget exceeded")
      }
      result.push((mask, value / total))
    }
  }
  { domain: f, focals: result, }
}

///|
pub fn Mass::frame(self : Mass) -> Frame {
  self.domain
}

///|
pub fn Mass::entries(self : Mass) -> Array[(Int32, Double)] {
  self.focals.copy()
}

///|
pub fn Mass::focal_count(self : Mass) -> Int32 {
  self.focals.length()
}

///|
pub fn Mass::weight(self : Mass, mask : Int32) -> Double raise BeliefError {
  self.domain.check_mask(mask)
  for (subset, value) in self.focals {
    if subset == mask {
      return value
    }
    if subset > mask {
      break
    }
  }
  0.0
}

///|
pub fn Mass::conflict(self : Mass) -> Double {
  if self.focals[0].0 == 0 {
    self.focals[0].1
  } else {
    0.0
  }
}