///|
/// Humidity: how much water the air holds, and the temperatures that say so.
///
/// Saturation vapour pressure is the pressure the water vapour in the air has
/// when the air holds as much as it can at that temperature. A dew point is
/// the temperature the air would have to be cooled to for that to happen, so
/// the two say the same thing in different words; relative humidity is how far
/// the air is from that state, the frost point is the dew point read against
/// ice rather than water, the wet-bulb temperature is what a wet thermometer
/// reads in moving air, and the absolute humidity is the mass of vapour in a
/// cubic metre.
///
/// They are all read off one Magnus form, from Alduchov and Eskridge (1996),
/// so that a vapour pressure and its dew point are exact inverses of each
/// other: `6.1094 · exp(17.625·T / (T + 243.04))` over water and
/// `6.1121 · exp(22.587·T / (T + 273.86))` over ice, both in hectopascals.
/// Bolton's more common `6.112 · exp(17.67·T / (T + 243.5))` differs from it
/// by less than half a percent at room temperature.
///
/// Temperatures are in degrees Celsius, relative humidity is a percentage and
/// pressures are in hectopascals.

///|
/// Air whose humidity will not do: a percentage that is not one, or a parcel
/// that is already supersaturated.
pub suberror HumidityError {
  /// A relative humidity is a percentage above zero and at most 100, and this
  /// one is not. The payload is the offending percentage. Air that holds no
  /// water at all has no dew point — it would have to be infinitely cold — so
  /// a humidity of zero is refused along with the percentages beyond the
  /// range.
  BadRelativeHumidity(Double)
  /// A parcel whose dew point is above its own temperature would be
  /// supersaturated, which no report of one means. The payloads are the dew
  /// point and the temperature, in degrees Celsius.
  DewPointAboveTemperature(Double, Double)
}

///|
/// The vapour pressure of water at 0 °C, in hectopascals: where the Magnus
/// curve over water passes through.
let water_reference_pressure : Double = 6.1094

///|
/// The pressure coefficient `a` of the Magnus form over water.
let water_pressure_coefficient : Double = 17.625

///|
/// The temperature coefficient `b` of the Magnus form over water, in degrees
/// Celsius.
let water_temperature_coefficient : Double = 243.04

///|
/// The vapour pressure of ice at 0 °C, in hectopascals.
let ice_reference_pressure : Double = 6.1121

///|
/// The pressure coefficient `a` of the Magnus form over ice.
let ice_pressure_coefficient : Double = 22.587

///|
/// The temperature coefficient `b` of the Magnus form over ice, in degrees
/// Celsius.
let ice_temperature_coefficient : Double = 273.86

///|
/// The coldest air Stull's wet-bulb fit is defined at, in degrees Celsius.
let wet_bulb_minimum_temperature : Double = -20.0

///|
/// The warmest air Stull's wet-bulb fit is defined at, in degrees Celsius.
let wet_bulb_maximum_temperature : Double = 50.0

///|
/// The driest air Stull's wet-bulb fit is defined at, in percent.
let wet_bulb_minimum_humidity : Double = 5.0

///|
/// The most humid air Stull's wet-bulb fit is defined at, in percent.
let wet_bulb_maximum_humidity : Double = 99.0

///|
/// The molar mass of water, in grams per mole.
let water_molar_mass : Double = 18.01528

///|
/// The molar gas constant, in joules per mole kelvin.
let ideal_gas_constant : Double = 8.314462618

///|
/// The vapour pressure of air saturated with water at `celsius`, in
/// hectopascals: how much pressure its water vapour would have if the air held
/// all it can at that temperature.
///
/// This is also the actual vapour pressure of air whose dew point is
/// `celsius`, which is how the relative humidity and the dew point are read
/// off it.
pub fn saturation_vapor_pressure(celsius : Double) -> Double {
  water_reference_pressure *
  @math.exp(
    water_pressure_coefficient *
    celsius /
    (celsius + water_temperature_coefficient),
  )
}

///|
/// The vapour pressure of air saturated with respect to ice at `celsius`, in
/// hectopascals.
///
/// Below freezing the air saturates against ice rather than water, and the
/// pressure over ice is the lower of the two — 2.597 hPa against 2.868 hPa at
/// −10 °C — so a frost point sits above the dew point at the same humidity.
pub fn saturation_vapor_pressure_over_ice(celsius : Double) -> Double {
  ice_reference_pressure *
  @math.exp(
    ice_pressure_coefficient * celsius / (celsius + ice_temperature_coefficient),
  )
}

///|
/// The dew point of air at `celsius` holding `relative_humidity` percent of
/// the water it could, in degrees Celsius: the temperature it would have to be
/// cooled to for the vapour to begin condensing.
///
/// The relative humidity has to be above zero and at most 100.
pub fn dew_point(
  celsius : Double,
  relative_humidity : Double,
) -> Double raise HumidityError {
  if relative_humidity <= 0.0 || relative_humidity > 100.0 {
    raise HumidityError::BadRelativeHumidity(relative_humidity)
  }
  let gamma = @math.ln(
    relative_humidity /
    100.0 *
    saturation_vapor_pressure(celsius) /
    water_reference_pressure,
  )
  water_temperature_coefficient * gamma / (water_pressure_coefficient - gamma)
}

///|
/// The relative humidity of air at `celsius` whose dew point is
/// `dew_point_celsius`, in percent: how much of the water it could hold it is
/// holding.
///
/// A dew point above the temperature would be supersaturated, which is
/// refused rather than reported as a humidity above 100.
pub fn relative_humidity(
  celsius : Double,
  dew_point_celsius : Double,
) -> Double raise HumidityError {
  if dew_point_celsius > celsius {
    raise HumidityError::DewPointAboveTemperature(dew_point_celsius, celsius)
  }
  100.0 *
  saturation_vapor_pressure(dew_point_celsius) /
  saturation_vapor_pressure(celsius)
}

///|
/// The frost point of air at `celsius` holding `relative_humidity` percent of
/// the water it could, in degrees Celsius: the temperature at which the vapour
/// would begin to deposit as ice.
///
/// Below freezing this sits above the dew point of the same air, because the
/// vapour pressure over ice is the lower of the two.
pub fn frost_point(
  celsius : Double,
  relative_humidity : Double,
) -> Double raise HumidityError {
  if relative_humidity <= 0.0 || relative_humidity > 100.0 {
    raise HumidityError::BadRelativeHumidity(relative_humidity)
  }
  let gamma = @math.ln(
    relative_humidity /
    100.0 *
    saturation_vapor_pressure(celsius) /
    ice_reference_pressure,
  )
  ice_temperature_coefficient * gamma / (ice_pressure_coefficient - gamma)
}

///|
/// The wet-bulb temperature of air at `celsius` holding `relative_humidity`
/// percent of the water it could, in degrees Celsius: what a thermometer reads
/// with a wet wick over it in moving air, which evaporation cools below the
/// air temperature.
///
/// The value is Stull's fit (2011), `T·atan(0.151977·√(RH + 8.313659)) +
/// atan(T + RH) − atan(RH − 1.676331) + 0.00391838·RH^1.5·atan(0.023101·RH) −
/// 4.686035`, with the angles in radians. It is fitted over 5 % to 99 %
/// humidity and −20 °C to 50 °C, and answers `None` outside that, where it is
/// an extrapolation. Saturated air is outside it too: the fit drifts there,
/// and the wet-bulb temperature of saturated air is the air temperature
/// itself.
pub fn wet_bulb_temperature(
  celsius : Double,
  relative_humidity : Double,
) -> Double? {
  if celsius < wet_bulb_minimum_temperature ||
    celsius > wet_bulb_maximum_temperature ||
    relative_humidity < wet_bulb_minimum_humidity ||
    relative_humidity > wet_bulb_maximum_humidity {
    return None
  }
  Some(
    celsius * @math.atan(0.151977 * (relative_humidity + 8.313659).sqrt()) +
    @math.atan(celsius + relative_humidity) -
    @math.atan(relative_humidity - 1.676331) +
    0.00391838 *
    @math.pow(relative_humidity, 1.5) *
    @math.atan(0.023101 * relative_humidity) -
    4.686035,
  )
}

///|
/// How much water a cubic metre of air at `celsius` holds when its vapour
/// pressure is `vapor_pressure_hpa`, in grams per cubic metre.
///
/// The vapour pressure is what the dew point is: `saturation_vapor_pressure`
/// of it gives the vapour pressure of saturated air, and half of that is air
/// at 50 % humidity.
pub fn absolute_humidity(
  vapor_pressure_hpa : Double,
  celsius : Double,
) -> Double {
  vapor_pressure_hpa *
  100.0 *
  water_molar_mass /
  (ideal_gas_constant * (celsius + 273.15))
}