///|
pub(all) struct LeverResult {
  first_fraction : Double
  second_fraction : Double
  first_amount : Double
  second_amount : Double
  projection_residual : Double
} derive(Debug, Eq, ToJson)

///|
/// Apply the lever rule by projecting an overall composition onto the line
/// between two phase endpoints. A small residual confirms collinearity.
pub fn lever_rule(
  overall : @domain.TernaryComposition,
  first : @domain.TernaryComposition,
  second : @domain.TernaryComposition,
  total_amount? : Double = 1.0,
  tolerance? : Double = 1.0e-8,
) -> LeverResult raise @domain.ExtractionError {
  @domain.require_positive("total amount", total_amount)
  @domain.require_positive("tolerance", tolerance)
  let dc = first.carrier - second.carrier
  let du = first.solute - second.solute
  let ds = first.solvent - second.solvent
  let denominator = dc * dc + du * du + ds * ds
  if denominator <= tolerance * tolerance {
    raise @domain.ExtractionError::SingularBalance(
      context="lever-rule endpoints coincide",
    )
  }
  let oc = overall.carrier - second.carrier
  let ou = overall.solute - second.solute
  let os = overall.solvent - second.solvent
  let first_fraction = (oc * dc + ou * du + os * ds) / denominator
  if first_fraction < -tolerance || first_fraction > 1.0 + tolerance {
    raise @domain.ExtractionError::OutOfRange(
      name="lever fraction",
      value=first_fraction,
      lower=0.0,
      upper=1.0,
    )
  }
  let bounded_first = first_fraction.clamp(min=0.0, max=1.0)
  let predicted = @domain.TernaryComposition::from_amounts(
    carrier=bounded_first * first.carrier +
      (1.0 - bounded_first) * second.carrier,
    solute=bounded_first * first.solute + (1.0 - bounded_first) * second.solute,
    solvent=bounded_first * first.solvent +
      (1.0 - bounded_first) * second.solvent,
  )
  let residual = predicted.distance_squared(overall).sqrt()
  if residual > tolerance {
    raise @domain.ExtractionError::InvalidArgument(
      message="overall composition is not on the phase-endpoint line; residual=\{residual}",
    )
  }
  {
    first_fraction: bounded_first,
    second_fraction: 1.0 - bounded_first,
    first_amount: total_amount * bounded_first,
    second_amount: total_amount * (1.0 - bounded_first),
    projection_residual: residual,
  }
}

///|
pub fn split_on_tie_line(
  overall : @domain.TernaryComposition,
  tie_line : @domain.TieLine,
  total_amount? : Double = 1.0,
  tolerance? : Double = 1.0e-8,
) -> @domain.PhaseSplit raise @domain.ExtractionError {
  let lever = lever_rule(
    overall,
    tie_line.raffinate,
    tie_line.extract,
    total_amount~,
    tolerance~,
  )
  {
    raffinate_amount: lever.first_amount,
    extract_amount: lever.second_amount,
    raffinate: tie_line.raffinate,
    extract: tie_line.extract,
    residual: lever.projection_residual,
  }
}