///|
pub struct PsychrometricPoint {
  dry_bulb_c : Double
  relative_humidity : Double
  humidity_ratio : Double
  enthalpy_kj_per_kg_dry : Double
  dew_point_c : Double
} derive(Debug)

///|
pub struct AirConditioningReport {
  inlet : PsychrometricPoint
  outlet : PsychrometricPoint
  dry_air_mass_flow_kg_per_s : Double
  cooling_duty_w : Double
  sensible_duty_w : Double
  latent_duty_w : Double
  condensate_kg_per_s : Double
  sensible_heat_ratio : Double
} derive(Debug)

///|
pub fn psychrometric_point(
  dry_bulb_c : Double,
  relative_humidity : Double,
  pressure_kpa : Double,
) -> PsychrometricPoint {
  let humidity_ratio = humidity_ratio_from_relative_humidity(
    dry_bulb_c, relative_humidity, pressure_kpa,
  )
  {
    dry_bulb_c,
    relative_humidity: relative_humidity.max(0.0).min(1.0),
    humidity_ratio,
    enthalpy_kj_per_kg_dry: moist_air_enthalpy_kj_per_kg_dry(
      dry_bulb_c, humidity_ratio,
    ),
    dew_point_c: dew_point_c(dry_bulb_c, humidity_ratio, pressure_kpa),
  }
}

///|
pub fn point_from_humidity_ratio(
  dry_bulb_c : Double,
  humidity_ratio : Double,
  pressure_kpa : Double,
) -> PsychrometricPoint {
  let ratio = humidity_ratio.max(0.0)
  {
    dry_bulb_c,
    relative_humidity: relative_humidity_from_ratio(
      dry_bulb_c, ratio, pressure_kpa,
    ),
    humidity_ratio: ratio,
    enthalpy_kj_per_kg_dry: moist_air_enthalpy_kj_per_kg_dry(dry_bulb_c, ratio),
    dew_point_c: dew_point_c(dry_bulb_c, ratio, pressure_kpa),
  }
}

///|
pub fn air_conditioning_report(
  inlet : PsychrometricPoint,
  outlet : PsychrometricPoint,
  dry_air_mass_flow_kg_per_s : Double,
) -> AirConditioningReport {
  let total = dry_air_mass_flow_kg_per_s.max(0.0) *
    (outlet.enthalpy_kj_per_kg_dry - inlet.enthalpy_kj_per_kg_dry) *
    1000.0
  let sensible = dry_air_mass_flow_kg_per_s.max(0.0) *
    (outlet.dry_bulb_c - inlet.dry_bulb_c) *
    moist_air_cp_kj_per_kg_k(inlet.humidity_ratio) *
    1000.0
  let latent = total - sensible
  let shr = if total.abs() < 1.0e-12 { 0.0 } else { sensible / total }
  {
    inlet,
    outlet,
    dry_air_mass_flow_kg_per_s,
    cooling_duty_w: total,
    sensible_duty_w: sensible,
    latent_duty_w: latent,
    condensate_kg_per_s: (dry_air_mass_flow_kg_per_s *
    (inlet.humidity_ratio - outlet.humidity_ratio)).max(0.0),
    sensible_heat_ratio: shr,
  }
}

///|
pub fn mix_air_points(
  first : PsychrometricPoint,
  first_mass_flow_kg_per_s : Double,
  second : PsychrometricPoint,
  second_mass_flow_kg_per_s : Double,
) -> PsychrometricPoint {
  let first_flow = first_mass_flow_kg_per_s.max(0.0)
  let second_flow = second_mass_flow_kg_per_s.max(0.0)
  let total = first_flow + second_flow
  if total <= 0.0 {
    first
  } else {
    let ratio = (
        first.humidity_ratio * first_flow + second.humidity_ratio * second_flow
      ) /
      total
    let enthalpy = (
        first.enthalpy_kj_per_kg_dry * first_flow +
        second.enthalpy_kj_per_kg_dry * second_flow
      ) /
      total
    let temperature = (enthalpy - 2501.0 * ratio) / (1.006 + 1.86 * ratio)
    point_from_humidity_ratio(temperature, ratio, 101.325)
  }
}

///|
pub fn heating_coil_point(
  inlet : PsychrometricPoint,
  target_temperature_c : Double,
) -> PsychrometricPoint {
  point_from_humidity_ratio(target_temperature_c, inlet.humidity_ratio, 101.325)
}

///|
pub fn humidification_point(
  inlet : PsychrometricPoint,
  target_humidity_ratio : Double,
) -> PsychrometricPoint {
  point_from_humidity_ratio(inlet.dry_bulb_c, target_humidity_ratio, 101.325)
}

///|
pub fn dehumidification_point(
  inlet : PsychrometricPoint,
  target_relative_humidity : Double,
) -> PsychrometricPoint {
  let saturated_ratio = humidity_ratio_from_relative_humidity(
    inlet.dry_bulb_c,
    target_relative_humidity,
    101.325,
  )
  point_from_humidity_ratio(
    inlet.dry_bulb_c,
    saturated_ratio.min(inlet.humidity_ratio),
    101.325,
  )
}

///|
pub fn sensible_heat_ratio(
  sensible_duty_w : Double,
  total_duty_w : Double,
) -> Double {
  if total_duty_w.abs() < 1.0e-12 {
    0.0
  } else {
    sensible_duty_w / total_duty_w
  }
}

///|
pub fn coil_bypass_factor(
  inlet_temperature_c : Double,
  outlet_temperature_c : Double,
  apparatus_dew_point_c : Double,
) -> Double {
  let denominator = inlet_temperature_c - apparatus_dew_point_c
  if denominator.abs() < 1.0e-12 {
    0.0
  } else {
    ((outlet_temperature_c - apparatus_dew_point_c) / denominator)
    .max(0.0)
    .min(1.0)
  }
}

///|
pub fn coil_contact_factor(bypass_factor : Double) -> Double {
  1.0 - bypass_factor.max(0.0).min(1.0)
}

///|
pub fn moisture_removal_rate(
  inlet_ratio : Double,
  outlet_ratio : Double,
  dry_air_mass_flow_kg_per_s : Double,
) -> Double {
  (inlet_ratio - outlet_ratio).max(0.0) * dry_air_mass_flow_kg_per_s.max(0.0)
}

///|
pub fn air_heater_duty_w(
  inlet : PsychrometricPoint,
  outlet_temperature_c : Double,
  dry_air_mass_flow_kg_per_s : Double,
) -> Double {
  let outlet = heating_coil_point(inlet, outlet_temperature_c)
  dry_air_mass_flow_kg_per_s.max(0.0) *
  (outlet.enthalpy_kj_per_kg_dry - inlet.enthalpy_kj_per_kg_dry) *
  1000.0
}

///|
pub fn air_mixing_enthalpy(
  points : Array[PsychrometricPoint],
  flows : Array[Double],
) -> Double {
  let length = points.length().min(flows.length())
  let mut total_flow = 0.0
  let mut total_enthalpy = 0.0
  for i in 0..