///|
pub(all) struct LeachingStageResult {
  equilibrium_concentration : Double
  retained_solution : Double
  overflow_solution : Double
  solute_in_underflow : Double
  solute_in_overflow : Double
  dissolved_recovery : Double
  balance_residual : Double
} derive(Debug, Eq, ToJson)

///|
/// Solve one ideal solid-liquid washing/leaching contact.
///
/// `retention_ratio` is kg solution retained per kg insoluble dry solid.
pub fn single_stage_leach(
  dry_solid_mass~ : Double,
  initial_solution_mass~ : Double,
  initial_solute_fraction~ : Double,
  wash_solution_mass~ : Double,
  wash_solute_fraction? : Double = 0.0,
  retention_ratio~ : Double,
) -> LeachingStageResult raise @domain.ExtractionError {
  @domain.require_positive("dry solid mass", dry_solid_mass)
  @domain.require_non_negative("initial solution mass", initial_solution_mass)
  @domain.require_fraction("initial solute fraction", initial_solute_fraction)
  @domain.require_non_negative("wash solution mass", wash_solution_mass)
  @domain.require_fraction("wash solute fraction", wash_solute_fraction)
  @domain.require_non_negative("retention ratio", retention_ratio)
  let total_solution = initial_solution_mass + wash_solution_mass
  if total_solution <= 0.0 {
    raise @domain.ExtractionError::InvalidFlow(
      name="total solution",
      value=total_solution,
    )
  }
  let inlet_solute = initial_solution_mass * initial_solute_fraction +
    wash_solution_mass * wash_solute_fraction
  let concentration = inlet_solute / total_solution
  let retained_capacity = retention_ratio * dry_solid_mass
  let retained = retained_capacity.min(total_solution)
  let overflow = total_solution - retained
  let solute_underflow = retained * concentration
  let solute_overflow = overflow * concentration
  {
    equilibrium_concentration: concentration,
    retained_solution: retained,
    overflow_solution: overflow,
    solute_in_underflow: solute_underflow,
    solute_in_overflow: solute_overflow,
    dissolved_recovery: if inlet_solute > 0.0 {
      solute_overflow / inlet_solute
    } else {
      0.0
    },
    balance_residual: inlet_solute - solute_underflow - solute_overflow,
  }
}