///|
/// A normalized three-component composition.
///
/// Components are ordered as carrier, solute and solvent. Fractions must be
/// finite, non-negative and sum to one within the supplied tolerance.
pub(all) struct TernaryComposition {
  carrier : Double
  solute : Double
  solvent : Double
} derive(Debug, Eq, ToJson)

///|
pub fn TernaryComposition::new(
  carrier~ : Double,
  solute~ : Double,
  solvent~ : Double,
  tolerance? : Double = 1.0e-9,
) -> TernaryComposition raise ExtractionError {
  require_fraction("carrier", carrier)
  require_fraction("solute", solute)
  require_fraction("solvent", solvent)
  let total = carrier + solute + solvent
  if (total - 1.0).abs() > tolerance {
    raise ExtractionError::InvalidComposition(total~)
  }
  { carrier, solute, solvent }
}

///|
/// Normalize non-negative component amounts to fractions.
pub fn TernaryComposition::from_amounts(
  carrier~ : Double,
  solute~ : Double,
  solvent~ : Double,
) -> TernaryComposition raise ExtractionError {
  require_non_negative("carrier amount", carrier)
  require_non_negative("solute amount", solute)
  require_non_negative("solvent amount", solvent)
  let total = carrier + solute + solvent
  if total <= 0.0 {
    raise ExtractionError::InvalidComposition(total~)
  }
  { carrier: carrier / total, solute: solute / total, solvent: solvent / total }
}

///|
pub fn TernaryComposition::component(
  self : TernaryComposition,
  index : Int,
) -> Double raise ExtractionError {
  match index {
    0 => self.carrier
    1 => self.solute
    2 => self.solvent
    _ =>
      raise ExtractionError::InvalidArgument(
        message="component index must be 0, 1 or 2",
      )
  }
}

///|
pub fn TernaryComposition::distance_squared(
  self : TernaryComposition,
  other : TernaryComposition,
) -> Double {
  let dc = self.carrier - other.carrier
  let du = self.solute - other.solute
  let ds = self.solvent - other.solvent
  dc * dc + du * du + ds * ds
}

///|
pub fn TernaryComposition::mix(
  self : TernaryComposition,
  other : TernaryComposition,
  self_amount~ : Double,
  other_amount~ : Double,
) -> TernaryComposition raise ExtractionError {
  require_non_negative("self amount", self_amount)
  require_non_negative("other amount", other_amount)
  TernaryComposition::from_amounts(
    carrier=self.carrier * self_amount + other.carrier * other_amount,
    solute=self.solute * self_amount + other.solute * other_amount,
    solvent=self.solvent * self_amount + other.solvent * other_amount,
  )
}