///|
/// Temperature scales supported by process calculations.
pub(all) enum TemperatureUnit {
  Celsius
  Kelvin
  Fahrenheit
} derive(Debug, Eq, ToJson)

///|
/// A non-negative absolute temperature with explicit units.
pub(all) struct Temperature {
  value : Double
  unit : TemperatureUnit
} derive(Debug, Eq, ToJson)

///|
pub fn Temperature::new(
  value~ : Double,
  unit~ : TemperatureUnit,
) -> Temperature raise @domain.ExtractionError {
  @domain.require_finite("temperature", value)
  let kelvin = match unit {
    Celsius => value + 273.15
    Kelvin => value
    Fahrenheit => (value - 32.0) * 5.0 / 9.0 + 273.15
  }
  if kelvin <= 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="absolute temperature must be above zero",
    )
  }
  { value, unit }
}

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

///|
pub fn Temperature::unit(self : Temperature) -> TemperatureUnit {
  self.unit
}

///|
pub fn Temperature::kelvin(self : Temperature) -> Double {
  match self.unit {
    Celsius => self.value + 273.15
    Kelvin => self.value
    Fahrenheit => (self.value - 32.0) * 5.0 / 9.0 + 273.15
  }
}

///|
pub fn Temperature::celsius(self : Temperature) -> Double {
  self.kelvin() - 273.15
}

///|
pub fn Temperature::fahrenheit(self : Temperature) -> Double {
  self.kelvin() * 9.0 / 5.0 - 459.67
}

///|
pub fn Temperature::convert(
  self : Temperature,
  unit : TemperatureUnit,
) -> Temperature {
  let value = match unit {
    Celsius => self.celsius()
    Kelvin => self.kelvin()
    Fahrenheit => self.fahrenheit()
  }
  { value, unit }
}

///|
pub fn Temperature::delta(self : Temperature, other : Temperature) -> Double {
  self.kelvin() - other.kelvin()
}

///|
pub fn Temperature::shift(
  self : Temperature,
  delta_kelvin : Double,
) -> Temperature {
  { value: self.kelvin() + delta_kelvin, unit: Kelvin }
}

///|
/// Pressure units used by equilibrium and equipment models.
pub(all) enum PressureUnit {
  Pascal
  KiloPascal
  Bar
  Atmosphere
  MillimeterHg
} derive(Debug, Eq, ToJson)

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

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

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

///|
pub fn Pressure::unit(self : Pressure) -> PressureUnit {
  self.unit
}

///|
pub fn Pressure::pascal(self : Pressure) -> Double {
  match self.unit {
    Pascal => self.value
    KiloPascal => self.value * 1000.0
    Bar => self.value * 100000.0
    Atmosphere => self.value * 101325.0
    MillimeterHg => self.value * 133.322387415
  }
}

///|
pub fn Pressure::kilopascal(self : Pressure) -> Double {
  self.pascal() / 1000.0
}

///|
pub fn Pressure::bar(self : Pressure) -> Double {
  self.pascal() / 100000.0
}

///|
pub fn Pressure::atmosphere(self : Pressure) -> Double {
  self.pascal() / 101325.0
}

///|
pub fn Pressure::millimeter_hg(self : Pressure) -> Double {
  self.pascal() / 133.322387415
}

///|
pub fn Pressure::convert(self : Pressure, unit : PressureUnit) -> Pressure {
  let value = match unit {
    Pascal => self.pascal()
    KiloPascal => self.kilopascal()
    Bar => self.bar()
    Atmosphere => self.atmosphere()
    MillimeterHg => self.millimeter_hg()
  }
  { value, unit }
}

///|
pub fn Pressure::drop(self : Pressure, downstream : Pressure) -> Pressure {
  let difference = self.pascal() - downstream.pascal()
  { value: difference.max(0.0), unit: Pascal }
}

///|
pub fn Pressure::ratio(self : Pressure, reference : Pressure) -> Double {
  self.pascal() / reference.pascal()
}

///|
/// Volumetric units for solvent and equipment sizing.
pub(all) enum VolumeUnit {
  CubicMeter
  Liter
  Milliliter
  Gallon
} derive(Debug, Eq, ToJson)

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

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

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

///|
pub fn Volume::cubic_meter(self : Volume) -> Double {
  match self.unit {
    CubicMeter => self.value
    Liter => self.value / 1000.0
    Milliliter => self.value / 1000000.0
    Gallon => self.value * 0.003785411784
  }
}

///|
pub fn Volume::liter(self : Volume) -> Double {
  self.cubic_meter() * 1000.0
}

///|
pub fn Volume::milliliter(self : Volume) -> Double {
  self.cubic_meter() * 1000000.0
}

///|
pub fn Volume::gallon(self : Volume) -> Double {
  self.cubic_meter() / 0.003785411784
}

///|
pub fn Volume::convert(self : Volume, unit : VolumeUnit) -> Volume {
  let value = match unit {
    CubicMeter => self.cubic_meter()
    Liter => self.liter()
    Milliliter => self.milliliter()
    Gallon => self.gallon()
  }
  { value, unit }
}

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

///|
pub fn Volume::scale(
  self : Volume,
  factor : Double,
) -> Volume raise @domain.ExtractionError {
  @domain.require_non_negative("volume scale", factor)
  { value: self.value * factor, unit: self.unit }
}

///|
/// Energy units for heating and cooling duty estimates.
pub(all) enum EnergyUnit {
  Joule
  KiloJoule
  MegaJoule
  KilowattHour
} derive(Debug, Eq, ToJson)

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

///|
pub fn Energy::new(
  value~ : Double,
  unit~ : EnergyUnit,
) -> Energy raise @domain.ExtractionError {
  @domain.require_finite("energy", value)
  { value, unit }
}

///|
pub fn Energy::joule(self : Energy) -> Double {
  match self.unit {
    Joule => self.value
    KiloJoule => self.value * 1000.0
    MegaJoule => self.value * 1000000.0
    KilowattHour => self.value * 3600000.0
  }
}

///|
pub fn Energy::kilojoule(self : Energy) -> Double {
  self.joule() / 1000.0
}

///|
pub fn Energy::megajoule(self : Energy) -> Double {
  self.joule() / 1000000.0
}

///|
pub fn Energy::kilowatt_hour(self : Energy) -> Double {
  self.joule() / 3600000.0
}

///|
pub fn Energy::convert(self : Energy, unit : EnergyUnit) -> Energy {
  let value = match unit {
    Joule => self.joule()
    KiloJoule => self.kilojoule()
    MegaJoule => self.megajoule()
    KilowattHour => self.kilowatt_hour()
  }
  { value, unit }
}

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

///|
pub fn Energy::scale(self : Energy, factor : Double) -> Energy {
  { value: self.value * factor, unit: self.unit }
}

///|
/// Power is energy delivered per unit time.
pub(all) struct Power {
  energy : Energy
  duration : Duration
} derive(Debug, Eq, ToJson)

///|
pub fn Power::new(energy~ : Energy, duration~ : Duration) -> Power {
  { energy, duration }
}

///|
pub fn Power::watt(self : Power) -> Double {
  self.energy.joule() / self.duration.per_second()
}

///|
pub fn Power::kilowatt(self : Power) -> Double {
  self.watt() / 1000.0
}

///|
pub fn Power::energy_for(self : Power, duration : Duration) -> Energy {
  { value: self.watt() * duration.per_second(), unit: Joule }
}

///|
/// A consistent operating point passed between process calculations.
pub(all) struct OperatingConditions {
  temperature : Temperature
  pressure : Pressure
  residence_time : Duration
} derive(Debug, Eq, ToJson)

///|
pub fn OperatingConditions::new(
  temperature~ : Temperature,
  pressure~ : Pressure,
  residence_time~ : Duration,
) -> OperatingConditions {
  { temperature, pressure, residence_time }
}

///|
pub fn OperatingConditions::temperature(
  self : OperatingConditions,
) -> Temperature {
  self.temperature
}

///|
pub fn OperatingConditions::pressure(self : OperatingConditions) -> Pressure {
  self.pressure
}

///|
pub fn OperatingConditions::residence_time(
  self : OperatingConditions,
) -> Duration {
  self.residence_time
}

///|
pub fn OperatingConditions::with_temperature(
  self : OperatingConditions,
  temperature : Temperature,
) -> OperatingConditions {
  { temperature, pressure: self.pressure, residence_time: self.residence_time }
}

///|
pub fn OperatingConditions::with_pressure(
  self : OperatingConditions,
  pressure : Pressure,
) -> OperatingConditions {
  {
    temperature: self.temperature,
    pressure,
    residence_time: self.residence_time,
  }
}

///|
pub fn OperatingConditions::with_residence_time(
  self : OperatingConditions,
  residence_time : Duration,
) -> OperatingConditions {
  { temperature: self.temperature, pressure: self.pressure, residence_time }
}

///|
/// Convert a volumetric flow into a residence time for a vessel.
pub fn residence_time_from_flow(
  vessel_volume~ : Volume,
  volumetric_flow~ : Volume,
  duration~ : Duration,
) -> Duration raise @domain.ExtractionError {
  @domain.require_positive("vessel volume", vessel_volume.cubic_meter())
  @domain.require_positive("volumetric flow", volumetric_flow.cubic_meter())
  let seconds = vessel_volume.cubic_meter() /
    volumetric_flow.cubic_meter() *
    duration.per_second()
  Duration::new(value=seconds, unit=Second)
}

///|
/// Convert a mass flow to a concentration ratio using a common mass basis.
pub fn mass_flow_concentration(solute : MassFlow, carrier : MassFlow) -> Double {
  solute.kg_per_second() / carrier.kg_per_second()
}

///|
pub fn pressure_drop_fraction(inlet : Pressure, outlet : Pressure) -> Double {
  let inlet_pa = inlet.pascal()
  if inlet_pa <= 0.0 {
    0.0
  } else {
    (inlet_pa - outlet.pascal()).max(0.0) / inlet_pa
  }
}

///|
pub fn temperature_rise(inlet : Temperature, outlet : Temperature) -> Double {
  outlet.kelvin() - inlet.kelvin()
}

///|
pub fn energy_balance_residual(
  inlet : Energy,
  outlet : Energy,
  duty : Energy,
) -> Double {
  inlet.joule() + duty.joule() - outlet.joule()
}

///|
pub fn thermal_duty_from_heat_capacity(
  mass_flow_kg_per_second~ : Double,
  heat_capacity_kj_per_kg_k~ : Double,
  inlet~ : Temperature,
  outlet~ : Temperature,
) -> Energy raise @domain.ExtractionError {
  @domain.require_non_negative("mass flow", mass_flow_kg_per_second)
  @domain.require_non_negative("heat capacity", heat_capacity_kj_per_kg_k)
  let duty = mass_flow_kg_per_second *
    heat_capacity_kj_per_kg_k *
    temperature_rise(inlet, outlet)
  Energy::new(value=duty, unit=KiloJoule)
}