///|
/// Explicit engineering quantities prevent accidental mixing of kg/h and kg/s.
pub(all) enum MassUnit {
  Kilogram
  Gram
  Pound
} derive(Debug, Eq, ToJson)

///|
pub(all) enum TimeUnit {
  Second
  Minute
  Hour
  Day
} derive(Debug, Eq, ToJson)

///|
pub(all) struct Mass {
  value : Double
  unit : MassUnit
} derive(Debug, Eq, ToJson)

///|
pub fn Mass::new(
  value~ : Double,
  unit~ : MassUnit,
) -> Mass raise @domain.ExtractionError {
  @domain.require_finite("mass", value)
  @domain.require_non_negative("mass", value)
  { value, unit }
}

///|
pub fn Mass::value(self : Mass) -> Double {
  self.value
}

///|
pub fn Mass::unit(self : Mass) -> MassUnit {
  self.unit
}

///|
fn mass_to_kg(unit : MassUnit, value : Double) -> Double {
  match unit {
    Kilogram => value
    Gram => value / 1000.0
    Pound => value * 0.45359237
  }
}

///|
fn kg_to_mass(unit : MassUnit, value : Double) -> Double {
  match unit {
    Kilogram => value
    Gram => value * 1000.0
    Pound => value / 0.45359237
  }
}

///|
pub fn Mass::convert(self : Mass, target : MassUnit) -> Mass {
  { value: kg_to_mass(target, mass_to_kg(self.unit, self.value)), unit: target }
}

///|
pub fn Mass::add(self : Mass, other : Mass) -> Mass {
  { value: self.value + other.convert(self.unit).value, unit: self.unit }
}

///|
pub(all) struct Duration {
  value : Double
  unit : TimeUnit
} derive(Debug, Eq, ToJson)

///|
pub fn Duration::new(
  value~ : Double,
  unit~ : TimeUnit,
) -> Duration raise @domain.ExtractionError {
  @domain.require_finite("duration", value)
  @domain.require_positive("duration", value)
  { value, unit }
}

///|
fn duration_to_seconds(unit : TimeUnit, value : Double) -> Double {
  match unit {
    Second => value
    Minute => value * 60.0
    Hour => value * 3600.0
    Day => value * 86400.0
  }
}

///|
pub fn Duration::per_second(self : Duration) -> Double {
  duration_to_seconds(self.unit, self.value)
}

///|
pub(all) struct MassFlow {
  mass : Mass
  duration : Duration
} derive(Debug, Eq, ToJson)

///|
pub fn MassFlow::new(mass~ : Mass, duration~ : Duration) -> MassFlow {
  { mass, duration }
}

///|
pub fn MassFlow::kg_per_second(self : MassFlow) -> Double {
  mass_to_kg(self.mass.unit, self.mass.value) / self.duration.per_second()
}

///|
pub fn MassFlow::kg_per_hour(self : MassFlow) -> Double {
  self.kg_per_second() * 3600.0
}

///|
pub fn MassFlow::scale(
  self : MassFlow,
  factor : Double,
) -> MassFlow raise @domain.ExtractionError {
  @domain.require_non_negative("flow scale", factor)
  {
    mass: Mass::{ value: self.mass.value * factor, unit: self.mass.unit },
    duration: self.duration,
  }
}

///|
pub(all) struct Concentration {
  solute_mass : Mass
  carrier_mass : Mass
} derive(Debug, Eq, ToJson)

///|
pub fn Concentration::new(
  solute_mass~ : Mass,
  carrier_mass~ : Mass,
) -> Concentration raise @domain.ExtractionError {
  if carrier_mass.value() <= 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="carrier mass must be positive",
    )
  }
  { solute_mass, carrier_mass }
}

///|
pub fn Concentration::ratio(self : Concentration) -> Double {
  mass_to_kg(self.solute_mass.unit, self.solute_mass.value) /
  mass_to_kg(self.carrier_mass.unit, self.carrier_mass.value)
}

///|
pub fn Concentration::mass_fraction(self : Concentration) -> Double {
  let solute = mass_to_kg(self.solute_mass.unit, self.solute_mass.value)
  let carrier = mass_to_kg(self.carrier_mass.unit, self.carrier_mass.value)
  solute / (solute + carrier)
}

///|
pub fn concentration_from_fraction(
  fraction : Double,
  carrier_mass : Mass,
) -> Concentration raise @domain.ExtractionError {
  if fraction < 0.0 || fraction >= 1.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="mass fraction must be in [0,1)",
    )
  }
  let carrier_kg = mass_to_kg(carrier_mass.unit, carrier_mass.value)
  let solute_kg = carrier_kg * fraction / (1.0 - fraction)
  Concentration::new(
    solute_mass=Mass::new(
      value=kg_to_mass(carrier_mass.unit, solute_kg),
      unit=carrier_mass.unit,
    ),
    carrier_mass~,
  )
}