///|
pub fn saturation_pressure_kpa(temperature_c : Double) -> Double {
  let exponent = 17.625 * temperature_c / (temperature_c + 243.04)
  0.61094 * @math.exp(exponent)
}

///|
pub fn saturation_pressure_pa(temperature_c : Double) -> Double {
  saturation_pressure_kpa(temperature_c) * 1000.0
}

///|
pub fn humidity_ratio_from_vapor_pressure(
  vapor_pressure_kpa : Double,
  total_pressure_kpa : Double,
) -> Double {
  if vapor_pressure_kpa <= 0.0 || total_pressure_kpa <= vapor_pressure_kpa {
    0.0
  } else {
    0.62198 * vapor_pressure_kpa / (total_pressure_kpa - vapor_pressure_kpa)
  }
}

///|
pub fn vapor_pressure_from_humidity_ratio(
  humidity_ratio : Double,
  total_pressure_kpa : Double,
) -> Double {
  if humidity_ratio <= 0.0 || total_pressure_kpa <= 0.0 {
    0.0
  } else {
    total_pressure_kpa * humidity_ratio / (0.62198 + humidity_ratio)
  }
}

///|
pub fn humidity_ratio_from_relative_humidity(
  dry_bulb_c : Double,
  relative_humidity : Double,
  total_pressure_kpa : Double,
) -> Double {
  let rh = relative_humidity.max(0.0).min(1.0)
  humidity_ratio_from_vapor_pressure(
    saturation_pressure_kpa(dry_bulb_c) * rh,
    total_pressure_kpa,
  )
}

///|
pub fn relative_humidity_from_ratio(
  dry_bulb_c : Double,
  humidity_ratio : Double,
  total_pressure_kpa : Double,
) -> Double {
  let saturation = saturation_pressure_kpa(dry_bulb_c)
  if saturation <= 0.0 {
    0.0
  } else {
    (vapor_pressure_from_humidity_ratio(humidity_ratio, total_pressure_kpa) /
    saturation)
    .max(0.0)
    .min(1.0)
  }
}

///|
pub fn moist_air_enthalpy_kj_per_kg_dry(
  dry_bulb_c : Double,
  humidity_ratio : Double,
) -> Double {
  1.006 * dry_bulb_c + humidity_ratio * (2501.0 + 1.86 * dry_bulb_c)
}

///|
pub fn moist_air_enthalpy_from_rh(
  dry_bulb_c : Double,
  relative_humidity : Double,
  total_pressure_kpa : Double,
) -> Double {
  let ratio = humidity_ratio_from_relative_humidity(
    dry_bulb_c, relative_humidity, total_pressure_kpa,
  )
  moist_air_enthalpy_kj_per_kg_dry(dry_bulb_c, ratio)
}

///|
pub fn dew_point_c(
  dry_bulb_c : Double,
  humidity_ratio : Double,
  total_pressure_kpa : Double,
) -> Double {
  let vapor = vapor_pressure_from_humidity_ratio(
    humidity_ratio, total_pressure_kpa,
  )
  if vapor <= 0.0 {
    -50.0.min(dry_bulb_c)
  } else {
    let alpha = @math.ln(vapor / 0.61094)
    (243.04 * alpha / (17.625 - alpha)).min(dry_bulb_c)
  }
}

///|
pub fn moist_air_density(
  dry_bulb_c : Double,
  humidity_ratio : Double,
  total_pressure_kpa : Double,
) -> Double {
  let temperature_k = celsius_to_kelvin(dry_bulb_c)
  if temperature_k <= 0.0 || total_pressure_kpa <= 0.0 {
    0.0
  } else {
    let dry_pressure_pa = kpa_to_pa(
      vapor_pressure_from_humidity_ratio(humidity_ratio, total_pressure_kpa),
    )
    let total_pressure_pa = kpa_to_pa(total_pressure_kpa)
    let vapor_pressure_pa = dry_pressure_pa
    (total_pressure_pa - vapor_pressure_pa) / (287.055 * temperature_k) +
    vapor_pressure_pa / (461.495 * temperature_k)
  }
}

///|
pub fn moist_air_specific_volume(
  dry_bulb_c : Double,
  humidity_ratio : Double,
  total_pressure_kpa : Double,
) -> Double {
  let density = moist_air_density(
    dry_bulb_c, humidity_ratio, total_pressure_kpa,
  )
  if density <= 0.0 {
    0.0
  } else {
    (1.0 + humidity_ratio) / density
  }
}

///|
pub fn moist_air_cp_kj_per_kg_k(humidity_ratio : Double) -> Double {
  1.006 + humidity_ratio.max(0.0) * 1.86
}

///|
pub fn wet_bulb_c(
  dry_bulb_c : Double,
  humidity_ratio : Double,
  total_pressure_kpa : Double,
) -> Double {
  let dew = dew_point_c(dry_bulb_c, humidity_ratio, total_pressure_kpa)
  let lower = dew.min(dry_bulb_c)
  let upper = dry_bulb_c.max(lower)
  let target = moist_air_enthalpy_kj_per_kg_dry(dry_bulb_c, humidity_ratio)
  let mut low = lower
  let mut high = upper
  for _ in 0..<48 {
    let middle = (low + high) / 2.0
    let saturated_ratio = humidity_ratio_from_relative_humidity(
      middle, 1.0, total_pressure_kpa,
    )
    let psychrometric_enthalpy = moist_air_enthalpy_kj_per_kg_dry(
      middle, saturated_ratio,
    )
    if psychrometric_enthalpy > target {
      high = middle
    } else {
      low = middle
    }
  }
  (low + high) / 2.0
}

///|
pub fn moisture_mass_flow(
  dry_air_mass_flow_kg_per_s : Double,
  humidity_ratio : Double,
) -> Double {
  dry_air_mass_flow_kg_per_s * humidity_ratio.max(0.0)
}

///|
pub fn humidification_water_duty(
  dry_air_mass_flow_kg_per_s : Double,
  from_ratio : Double,
  to_ratio : Double,
) -> Double {
  dry_air_mass_flow_kg_per_s * (to_ratio - from_ratio).max(0.0)
}

///|
pub fn evaporative_cooling_duty(
  dry_air_mass_flow_kg_per_s : Double,
  from_temperature_c : Double,
  to_temperature_c : Double,
  from_ratio : Double,
  to_ratio : Double,
) -> Double {
  let from_h = moist_air_enthalpy_kj_per_kg_dry(from_temperature_c, from_ratio)
  let to_h = moist_air_enthalpy_kj_per_kg_dry(to_temperature_c, to_ratio)
  dry_air_mass_flow_kg_per_s * (to_h - from_h) * 1000.0
}