///|
/// Strategies for distributing a total RSS tolerance budget.
pub enum AllocationStrategy {
  Equal
  Proportional
  Sensitivity
} derive(Debug, Eq)

///|
/// Create an equal-contribution allocation strategy.
pub fn equal_allocation() -> AllocationStrategy {
  Equal
}

///|
/// Create a strategy proportional to each current tolerance.
pub fn proportional_allocation() -> AllocationStrategy {
  Proportional
}

///|
/// Create a strategy that emphasizes contributors with larger variance.
pub fn sensitivity_allocation() -> AllocationStrategy {
  Sensitivity
}

///|
/// One allocated dimension in a tolerance budget plan.
pub struct AllocationItem {
  name : String
  nominal : Double
  allocated_tolerance : Double
  variance_fraction : Double
  weight : Double
} derive(Debug, Eq)

///|
/// A complete RSS allocation and its resulting worst-case span.
pub struct AllocationReport {
  items : Array[AllocationItem]
  total_nominal : Double
  total_rss : Double
  worst_case_span : Double
  strategy : AllocationStrategy
} derive(Debug, Eq)

///|
fn allocation_weight(
  dimension : Dimension,
  strategy : AllocationStrategy,
) -> Double {
  match strategy {
    Equal => 1.0
    Proportional => dimension.tolerance
    Sensitivity => dimension.tolerance * dimension.tolerance
  }
}

///|
fn sum_squared(values : Array[Double]) -> Double {
  let mut total = 0.0
  for value in values {
    total += value * value
  }
  total
}

///|
fn allocation_total_nominal(chain : Chain) -> Double {
  chain.nominal()
}

///|
/// Allocate an RSS budget while preserving contributor names and nominal values.
pub fn allocate_chain(
  chain : Chain,
  total_rss : Double,
  strategy : AllocationStrategy,
) -> AllocationReport {
  if total_rss < 0.0 {
    abort("total RSS budget must be non-negative")
  }
  let weights = chain.dimensions.map(d => allocation_weight(d, strategy))
  let squared = sum_squared(weights)
  let effective_weights = if squared == 0.0 {
    weights.map(_ => 1.0)
  } else {
    weights
  }
  let normalizer = sum_squared(effective_weights).sqrt()
  let scale = if normalizer == 0.0 { 0.0 } else { total_rss / normalizer }
  let total_variance = total_rss * total_rss
  let items = []
  let mut worst_case_span = 0.0
  for index in 0.. item.allocated_tolerance)).sqrt(),
    worst_case_span,
    strategy,
  }
}

///|
/// Return the conservative interval resulting from an allocation plan.
pub fn AllocationReport::interval(self : AllocationReport) -> Interval {
  Interval::new(
    self.total_nominal - self.worst_case_span,
    self.total_nominal + self.worst_case_span,
  )
}

///|
/// Return the largest allocated contributor.
pub fn AllocationReport::largest_contributor(
  self : AllocationReport,
) -> AllocationItem {
  let mut largest = self.items[0]
  for item in self.items {
    if item.allocated_tolerance > largest.allocated_tolerance {
      largest = item
    }
  }
  largest
}

///|
/// Allocate the same tolerance to every contributor for a requested RSS budget.
pub fn equal_tolerance_for_budget(
  chain : Chain,
  total_rss : Double,
) -> AllocationReport {
  allocate_chain(chain, total_rss, Equal)
}

///|
/// Allocate the requested RSS budget according to current tolerance sensitivity.
pub fn sensitivity_tolerance_for_budget(
  chain : Chain,
  total_rss : Double,
) -> AllocationReport {
  allocate_chain(chain, total_rss, Sensitivity)
}