///|
pub(all) struct ComponentProperty {
  name : String
  molecular_weight : Double
  density : Double
  boiling_temperature : Double
  heat_capacity : Double
  viscosity : Double
} derive(Debug, Eq, ToJson)

///|
pub(all) struct MixtureProperty {
  temperature : Double
  pressure : Double
  composition : Array[Double]
  molecular_weight : Double
  density : Double
  heat_capacity : Double
  viscosity : Double
  boiling_temperature : Double
} derive(Debug, Eq, ToJson)

///|
pub(all) struct PropertySensitivity {
  component : String
  composition_change : Double
  density_change : Double
  viscosity_change : Double
  boiling_change : Double
} derive(Debug, Eq, ToJson)

///|
pub fn validate_composition(composition : ArrayView[Double]) -> Bool {
  if composition.length() == 0 {
    return false
  }
  let mut total = 0.0
  for value in composition {
    if value < 0.0 {
      return false
    }
    total += value
  }
  (total - 1.0).abs() <= 1.0e-8
}

///|
pub fn mixture_normalize_composition(
  composition : ArrayView[Double],
) -> Array[Double] raise @domain.ExtractionError {
  if composition.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  let mut total = 0.0
  for value in composition {
    if value < 0.0 {
      raise @domain.ExtractionError::InvalidFraction(name="composition", value~)
    }
    total += value
  }
  if total == 0.0 {
    raise @domain.ExtractionError::InvalidComposition(total~)
  }
  [
    for value in composition => value / total
  ]
}

///|
pub fn mixture_molecular_weight(
  components : ArrayView[ComponentProperty],
  composition : ArrayView[Double],
) -> Double raise @domain.ExtractionError {
  if components.length() != composition.length() || components.length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="component composition dimensions differ",
    )
  }
  let normalized = mixture_normalize_composition(composition)
  let mut result = 0.0
  for index in 0.. Double raise @domain.ExtractionError {
  if components.length() != composition.length() || components.length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="component composition dimensions differ",
    )
  }
  let normalized = mixture_normalize_composition(composition)
  let mut reciprocal = 0.0
  for index in 0.. Double raise @domain.ExtractionError {
  if components.length() != composition.length() || components.length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="component composition dimensions differ",
    )
  }
  let normalized = mixture_normalize_composition(composition)
  let mut result = 0.0
  for index in 0.. Double raise @domain.ExtractionError {
  if components.length() != composition.length() || components.length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="component composition dimensions differ",
    )
  }
  let normalized = mixture_normalize_composition(composition)
  let mut logarithm = 0.0
  for index in 0.. Double raise @domain.ExtractionError {
  if components.length() != composition.length() || components.length() == 0 {
    return 0.0
  }
  let normalized = mixture_normalize_composition(composition)
  let mut result = 0.0
  for index in 0.. MixtureProperty raise @domain.ExtractionError {
  let normalized = mixture_normalize_composition(composition)
  {
    temperature,
    pressure,
    composition: normalized,
    molecular_weight: mixture_molecular_weight(components, composition),
    density: mixture_density(components, composition),
    heat_capacity: mixture_heat_capacity(components, composition),
    viscosity: mixture_viscosity(components, composition),
    boiling_temperature: mixture_boiling_temperature(components, composition),
  }
}

///|
pub fn component_contribution(
  property : ComponentProperty,
  composition : Double,
) -> (Double, Double, Double, Double) {
  (
    property.density * composition,
    property.viscosity * composition,
    property.heat_capacity * composition,
    property.boiling_temperature * composition,
  )
}

///|
pub fn mixture_excess_density(
  mixture : MixtureProperty,
  ideal_density : Double,
) -> Double {
  mixture.density - ideal_density
}

///|
pub fn mixture_density_factor(
  mixture : MixtureProperty,
  reference_density : Double,
) -> Double {
  if reference_density == 0.0 {
    0.0
  } else {
    mixture.density / reference_density
  }
}

///|
pub fn mixture_viscosity_factor(
  mixture : MixtureProperty,
  reference_viscosity : Double,
) -> Double {
  if reference_viscosity == 0.0 {
    0.0
  } else {
    mixture.viscosity / reference_viscosity
  }
}

///|
pub fn mixture_temperature_shift(
  mixture : MixtureProperty,
  target_temperature : Double,
  thermal_expansion : Double,
) -> MixtureProperty {
  let shift = target_temperature - mixture.temperature
  {
    temperature: target_temperature,
    pressure: mixture.pressure,
    composition: mixture.composition,
    molecular_weight: mixture.molecular_weight,
    density: mixture.density * (1.0 - thermal_expansion * shift).max(0.01),
    heat_capacity: mixture.heat_capacity,
    viscosity: mixture.viscosity * (1.0 - 0.01 * shift).max(0.05),
    boiling_temperature: mixture.boiling_temperature,
  }
}

///|
pub fn mixture_pressure_shift(
  mixture : MixtureProperty,
  target_pressure : Double,
  compressibility : Double,
) -> MixtureProperty {
  let ratio = if mixture.pressure == 0.0 {
    1.0
  } else {
    target_pressure / mixture.pressure
  }
  {
    temperature: mixture.temperature,
    pressure: target_pressure,
    composition: mixture.composition,
    molecular_weight: mixture.molecular_weight,
    density: mixture.density * (1.0 + compressibility * (ratio - 1.0)),
    heat_capacity: mixture.heat_capacity,
    viscosity: mixture.viscosity,
    boiling_temperature: mixture.boiling_temperature,
  }
}

///|
pub fn property_sensitivity(
  baseline : MixtureProperty,
  perturbed : MixtureProperty,
  component : String,
  composition_change : Double,
) -> PropertySensitivity {
  {
    component,
    composition_change,
    density_change: perturbed.density - baseline.density,
    viscosity_change: perturbed.viscosity - baseline.viscosity,
    boiling_change: perturbed.boiling_temperature - baseline.boiling_temperature,
  }
}

///|
pub fn mixture_component_count(mixture : MixtureProperty) -> Int {
  mixture.composition.length()
}

///|
pub fn mixture_dominant_component(mixture : MixtureProperty) -> Int {
  if mixture.composition.length() == 0 {
    return -1
  }
  let mut selected = 0
  for index in 1.. mixture.composition[selected] {
      selected = index
    }
  }
  selected
}

///|
pub fn mixture_minor_fraction(mixture : MixtureProperty) -> Double {
  if mixture.composition.length() == 0 {
    return 0.0
  }
  let dominant = mixture.composition[mixture_dominant_component(mixture)]
  1.0 - dominant
}

///|
pub fn mixture_composition_distance(
  first : MixtureProperty,
  second : MixtureProperty,
) -> Double {
  let count = first.composition.length().min(second.composition.length())
  let mut total = 0.0
  for index in 0.. String {
  let lines = [
    "temperature,pressure,molecular_weight,density,heat_capacity,viscosity,boiling_temperature",
  ]
  for property in properties {
    lines.push(
      property.temperature.to_string() +
      "," +
      property.pressure.to_string() +
      "," +
      property.molecular_weight.to_string() +
      "," +
      property.density.to_string() +
      "," +
      property.heat_capacity.to_string() +
      "," +
      property.viscosity.to_string() +
      "," +
      property.boiling_temperature.to_string(),
    )
  }
  lines.join("\n")
}

///|
pub fn mixture_property_report(property : MixtureProperty) -> String {
  "T=" +
  property.temperature.to_string() +
  ",P=" +
  property.pressure.to_string() +
  ",MW=" +
  property.molecular_weight.to_string() +
  ",density=" +
  property.density.to_string() +
  ",viscosity=" +
  property.viscosity.to_string() +
  ",dominant_component=" +
  mixture_dominant_component(property).to_string()
}

///|
fn mixture_approximate_log(value : Double) -> Double {
  if value <= 0.0 {
    return -30.0
  }
  let scaled = if value > 1.0 { value } else { 1.0 / value }
  let mut exponent = 0
  let mut current = scaled
  while current >= 2.0 && exponent < 30 {
    current /= 2.0
    exponent += 1
  }
  while current < 1.0 && exponent > -30 {
    current *= 2.0
    exponent -= 1
  }
  let z = (current - 1.0) / (current + 1.0)
  let z2 = z * z
  let series = 2.0 * (z + z * z2 / 3.0 + z * z2 * z2 / 5.0)
  if value > 1.0 {
    series + exponent.to_double() * 0.69314718056
  } else {
    -series - exponent.to_double() * 0.69314718056
  }
}

///|
fn mixture_approximate_exp(value : Double) -> Double {
  let bounded = value.max(-30.0).min(30.0)
  let mut result = 1.0
  let mut term = 1.0
  for index in 1..<18 {
    term *= bounded / index.to_double()
    result += term
  }
  result.max(0.0)
}