///|
/// Tie lines ordered by increasing raffinate-phase solute fraction.
pub(all) struct TieLineTable {
  lines : Array[@domain.TieLine]
} derive(Debug, Eq)

///|
pub fn TieLineTable::new(
  lines : ArrayView[@domain.TieLine],
) -> TieLineTable raise @domain.ExtractionError {
  if lines.length() < 2 {
    raise @domain.ExtractionError::InsufficientData(
      required=2,
      actual=lines.length(),
    )
  }
  for index in 1.. @domain.TernaryComposition raise @domain.ExtractionError {
  @domain.TernaryComposition::from_amounts(
    carrier=first.carrier + fraction * (second.carrier - first.carrier),
    solute=first.solute + fraction * (second.solute - first.solute),
    solvent=first.solvent + fraction * (second.solvent - first.solvent),
  )
}

///|
pub fn TieLineTable::at_raffinate_solute(
  self : TieLineTable,
  solute_fraction : Double,
) -> @domain.TieLine raise @domain.ExtractionError {
  @domain.require_fraction("raffinate solute fraction", solute_fraction)
  let first = self.lines[0].raffinate.solute
  let last = self.lines[self.lines.length() - 1].raffinate.solute
  if solute_fraction < first || solute_fraction > last {
    raise @domain.ExtractionError::OutOfRange(
      name="raffinate solute fraction",
      value=solute_fraction,
      lower=first,
      upper=last,
    )
  }
  for index in 0..<(self.lines.length() - 1) {
    let left = self.lines[index]
    let right = self.lines[index + 1]
    if solute_fraction >= left.raffinate.solute &&
      solute_fraction <= right.raffinate.solute {
      let fraction = (solute_fraction - left.raffinate.solute) /
        (right.raffinate.solute - left.raffinate.solute)
      return @domain.TieLine::new(
        raffinate=interpolate_composition(
          left.raffinate,
          right.raffinate,
          fraction,
        ),
        extract=interpolate_composition(left.extract, right.extract, fraction),
      )
    }
  }
  self.lines[self.lines.length() - 1]
}

///|
/// Select the measured tie line whose segment is closest to an overall point.
pub fn TieLineTable::nearest(
  self : TieLineTable,
  overall : @domain.TernaryComposition,
) -> @domain.TieLine {
  let mut best = self.lines[0]
  let mut best_residual = 1.0e300
  for line in self.lines {
    let a = line.raffinate
    let b = line.extract
    let dc = a.carrier - b.carrier
    let du = a.solute - b.solute
    let ds = a.solvent - b.solvent
    let denominator = dc * dc + du * du + ds * ds
    let fraction = (if denominator > 0.0 {
      (
        (overall.carrier - b.carrier) * dc +
        (overall.solute - b.solute) * du +
        (overall.solvent - b.solvent) * ds
      ) /
      denominator
    } else {
      0.0
    }).clamp(min=0.0, max=1.0)
    let pc = b.carrier + fraction * dc
    let pu = b.solute + fraction * du
    let ps = b.solvent + fraction * ds
    let residual = (pc - overall.carrier) * (pc - overall.carrier) +
      (pu - overall.solute) * (pu - overall.solute) +
      (ps - overall.solvent) * (ps - overall.solvent)
    if residual < best_residual {
      best = line
      best_residual = residual
    }
  }
  best
}