///|
fn validation_result(
  error_count : Int,
  warning_count : Int,
  messages : Array[String],
) -> ValidationSummary {
  ValidationSummary::{
    valid: error_count == 0,
    error_count,
    warning_count,
    messages,
  }
}

///|
pub fn validate_thermo_inputs(
  components~ : Array[Component],
  composition~ : Array[Double],
  temperature_k~ : Double,
  pressure_bar~ : Double,
) -> ValidationSummary {
  let messages : Array[String] = []
  if components.length() == 0 {
    messages.push("components cannot be empty")
  }
  if composition.length() == 0 {
    messages.push("composition cannot be empty")
  }
  let length_error = if components.length() != composition.length() {
    1
  } else {
    0
  }
  if length_error > 0 {
    messages.push("component and composition lengths differ")
  }
  let negative_count = for i = 0, count = 0; i < composition.length(); {
    if composition[i] < 0.0 {
      messages.push("composition contains a negative fraction")
      continue i + 1, count + 1
    } else {
      continue i + 1, count
    }
  } nobreak {
    count
  }
  let total = for i = 0, value = 0.0; i < composition.length(); {
    continue i + 1, value + composition[i]
  } nobreak {
    value
  }
  let sum_error = if composition.length() > 0 &&
    abs_double(total - 1.0) > 1.0E-8 {
    messages.push("composition is not normalized")
    1
  } else {
    0
  }
  let temperature_error = if temperature_k <= 0.0 {
    messages.push("temperature must be positive")
    1
  } else {
    0
  }
  let pressure_error = if pressure_bar <= 0.0 {
    messages.push("pressure must be positive")
    1
  } else {
    0
  }
  let range_warnings = for i = 0, count = 0; i < components.length(); {
    if !components[i].antoine.is_in_range(temperature_k) {
      messages.push(
        components[i].name + ": temperature is outside Antoine range",
      )
      continue i + 1, count + 1
    } else {
      continue i + 1, count
    }
  } nobreak {
    count
  }
  validation_result(
    length_error +
    negative_count +
    sum_error +
    temperature_error +
    pressure_error,
    range_warnings,
    messages,
  )
}

///|
pub fn validate_composition_for_model(
  composition : Array[Double],
  expected_dimension : Int,
) -> ValidationSummary {
  let messages : Array[String] = []
  let dimension_error = if composition.length() != expected_dimension {
    1
  } else {
    0
  }
  if dimension_error > 0 {
    messages.push("composition dimension does not match model")
  }
  let negative_count = for i = 0, count = 0; i < composition.length(); {
    if composition[i] < 0.0 {
      messages.push("composition fraction is negative")
      continue i + 1, count + 1
    } else {
      continue i + 1, count
    }
  } nobreak {
    count
  }
  let total = for i = 0, value = 0.0; i < composition.length(); {
    continue i + 1, value + composition[i]
  } nobreak {
    value
  }
  let sum_error = if composition.length() > 0 &&
    abs_double(total - 1.0) > 1.0E-8 {
    messages.push("composition fractions do not sum to one")
    1
  } else {
    0
  }
  validation_result(dimension_error + negative_count + sum_error, 0, messages)
}

///|
pub fn validate_thermo_property(property : ThermoProperty) -> ValidationSummary {
  let messages : Array[String] = []
  let warning_count = 0
  if property.molecular_weight_g_per_mol <= 0.0 {
    messages.push(
      property.component.name + ": molecular weight is not positive",
    )
  }
  if property.latent_heat_j_per_mol <= 0.0 {
    messages.push(property.component.name + ": latent heat is not positive")
  }
  let cp_sample_error = try {
    ignore(
      property.liquid_heat_capacity.evaluate(property.reference_temperature_k),
    )
    0
  } catch {
    _ => {
      messages.push(
        property.component.name + ": liquid heat capacity cannot be evaluated",
      )
      1
    }
  } noraise {
    _ => 0
  }
  let positivity_error = if property.molecular_weight_g_per_mol <= 0.0 {
    1
  } else {
    0
  }
  let latent_error = if property.latent_heat_j_per_mol <= 0.0 { 1 } else { 0 }
  validation_result(
    cp_sample_error + positivity_error + latent_error,
    warning_count,
    messages,
  )
}

///|
pub fn validate_property_catalog(
  properties : Array[ThermoProperty],
) -> ValidationSummary {
  let messages : Array[String] = []
  let errors = for i = 0, count = 0; i < properties.length(); {
    let summary = validate_thermo_property(properties[i])
    for message in summary.messages {
      messages.push(message)
    }
    continue i + 1, count + summary.error_count
  } nobreak {
    count
  }
  validation_result(errors, 0, messages)
}

///|
pub fn merge_validation_summaries(
  first : ValidationSummary,
  second : ValidationSummary,
) -> ValidationSummary {
  let messages : Array[String] = []
  for message in first.messages {
    messages.push(message)
  }
  for message in second.messages {
    messages.push(message)
  }
  validation_result(
    first.error_count + second.error_count,
    first.warning_count + second.warning_count,
    messages,
  )
}

///|
pub fn validation_risk_score(summary : ValidationSummary) -> Double {
  summary.error_count.to_double() * 10.0 + summary.warning_count.to_double()
}

///|
pub fn validation_is_strict(summary : ValidationSummary) -> Bool {
  summary.valid && summary.warning_count == 0
}

///|
pub fn validation_message_count(summary : ValidationSummary) -> Int {
  summary.messages.length()
}

///|
pub fn validate_phase_point(point : PhasePoint) -> ValidationSummary {
  let composition = validate_composition_for_model(
    point.composition,
    point.composition.length(),
  )
  let messages : Array[String] = []
  for message in composition.messages {
    messages.push(message)
  }
  let temperature_error = if point.temperature_k <= 0.0 {
    messages.push("phase-point temperature is not positive")
    1
  } else {
    0
  }
  let pressure_error = if point.pressure_bar <= 0.0 {
    messages.push("phase-point pressure is not positive")
    1
  } else {
    0
  }
  validation_result(
    composition.error_count + temperature_error + pressure_error,
    composition.warning_count,
    messages,
  )
}

///|
pub fn validate_binary_parameters(
  first : Double,
  second : Double,
  tolerance : Double,
) -> ValidationSummary {
  let messages : Array[String] = []
  let error_count = if tolerance <= 0.0 {
    messages.push("parameter tolerance must be positive")
    1
  } else {
    0
  }
  let warning_count = if abs_double(first) < tolerance ||
    abs_double(second) < tolerance {
    messages.push("one binary parameter is close to zero")
    1
  } else {
    0
  }
  validation_result(error_count, warning_count, messages)
}

///|
pub fn validate_solver_options(options : SolverOptions) -> ValidationSummary {
  let messages : Array[String] = []
  let error_count = if options.tolerance <= 0.0 || options.max_iterations <= 0 {
    messages.push("solver options are invalid")
    1
  } else {
    0
  }
  validation_result(error_count, 0, messages)
}

///|
pub fn validate_temperature_range(
  temperature_k : Double,
  low_k : Double,
  high_k : Double,
) -> ValidationSummary {
  let messages : Array[String] = []
  let invalid_range = low_k <= 0.0 || high_k <= low_k
  let out_of_range = temperature_k < low_k || temperature_k > high_k
  if invalid_range {
    messages.push("temperature range is invalid")
  }
  if out_of_range {
    messages.push("temperature is outside requested range")
  }
  validation_result(
    (if invalid_range { 1 } else { 0 }) + (if out_of_range { 1 } else { 0 }),
    0,
    messages,
  )
}

///|
pub fn validate_pressure_range(
  pressure_bar : Double,
  low_bar : Double,
  high_bar : Double,
) -> ValidationSummary {
  let messages : Array[String] = []
  let invalid_range = low_bar <= 0.0 || high_bar <= low_bar
  let out_of_range = pressure_bar < low_bar || pressure_bar > high_bar
  if invalid_range {
    messages.push("pressure range is invalid")
  }
  if out_of_range {
    messages.push("pressure is outside requested range")
  }
  validation_result(
    (if invalid_range { 1 } else { 0 }) + (if out_of_range { 1 } else { 0 }),
    0,
    messages,
  )
}

///|
pub fn validate_source_metadata(
  source_name : String,
  source_description : String,
) -> ValidationSummary {
  let messages : Array[String] = []
  let missing_name = source_name.trim().length() == 0
  let missing_description = source_description.trim().length() == 0
  if missing_name {
    messages.push("source name is missing")
  }
  if missing_description {
    messages.push("source description is missing")
  }
  validation_result(
    (if missing_name { 1 } else { 0 }) +
    (if missing_description { 1 } else { 0 }),
    0,
    messages,
  )
}

///|
pub fn validate_correlation_pair(
  lower_temperature_k : Double,
  upper_temperature_k : Double,
  lower_pressure_bar : Double,
  upper_pressure_bar : Double,
) -> ValidationSummary {
  let messages : Array[String] = []
  let temperature_error = if lower_temperature_k <= 0.0 ||
    upper_temperature_k <= lower_temperature_k {
    messages.push("correlation temperatures are invalid")
    1
  } else {
    0
  }
  let pressure_error = if lower_pressure_bar <= 0.0 || upper_pressure_bar <= 0.0 {
    messages.push("correlation pressures are invalid")
    1
  } else {
    0
  }
  let monotonic_warning = if upper_temperature_k > lower_temperature_k &&
    upper_pressure_bar <= lower_pressure_bar {
    messages.push("pressure is not increasing with temperature")
    1
  } else {
    0
  }
  validation_result(
    temperature_error + pressure_error,
    monotonic_warning,
    messages,
  )
}

///|
pub fn validate_pressure_prediction(
  reference_pressure_bar : Double,
  predicted_pressure_bar : Double,
  tolerance_bar : Double,
) -> ValidationSummary {
  let messages : Array[String] = []
  let input_error = if reference_pressure_bar <= 0.0 ||
    predicted_pressure_bar <= 0.0 ||
    tolerance_bar < 0.0 {
    messages.push("pressure prediction inputs are invalid")
    1
  } else {
    0
  }
  let difference = abs_double(predicted_pressure_bar - reference_pressure_bar)
  let warning = if input_error == 0 && difference > tolerance_bar {
    messages.push("pressure prediction exceeds tolerance")
    1
  } else {
    0
  }
  validation_result(input_error, warning, messages)
}

///|
pub fn validation_has_warning(summary : ValidationSummary) -> Bool {
  summary.warning_count > 0
}

///|
pub fn validation_has_errors(summary : ValidationSummary) -> Bool {
  summary.error_count > 0
}

///|
pub fn validation_summary_is_empty(summary : ValidationSummary) -> Bool {
  summary.error_count == 0 &&
  summary.warning_count == 0 &&
  summary.messages.length() == 0
}

///|
pub fn validation_score_passes(
  summary : ValidationSummary,
  maximum_score : Double,
) -> Bool {
  maximum_score >= 0.0 && validation_risk_score(summary) <= maximum_score
}

///|
pub fn validate_component_name(name : String) -> ValidationSummary {
  let messages : Array[String] = []
  let missing = name.trim().length() == 0
  let too_long = name.length() > 128
  if missing {
    messages.push("component name is missing")
  }
  if too_long {
    messages.push("component name is too long")
  }
  validation_result(
    (if missing { 1 } else { 0 }) + (if too_long { 1 } else { 0 }),
    0,
    messages,
  )
}

///|
pub fn validate_temperature_delta(
  temperature_delta_k : Double,
  maximum_delta_k : Double,
) -> ValidationSummary {
  let messages : Array[String] = []
  let invalid = temperature_delta_k <= 0.0 ||
    maximum_delta_k <= 0.0 ||
    temperature_delta_k > maximum_delta_k
  if invalid {
    messages.push("temperature delta is outside allowed bounds")
  }
  validation_result(if invalid { 1 } else { 0 }, 0, messages)
}

///|
pub fn validate_unit_name(unit : String) -> ValidationSummary {
  let messages : Array[String] = []
  let missing = unit.trim().length() == 0
  let too_long = unit.length() > 32
  if missing {
    messages.push("unit name is missing")
  }
  if too_long {
    messages.push("unit name is too long")
  }
  validation_result(
    (if missing { 1 } else { 0 }) + (if too_long { 1 } else { 0 }),
    0,
    messages,
  )
}

///|
pub fn validate_array_length(actual : Int, expected : Int) -> ValidationSummary {
  let messages : Array[String] = []
  let invalid = actual < 0 || expected < 0 || actual != expected
  if invalid {
    messages.push("array length does not match expected dimension")
  }
  validation_result(if invalid { 1 } else { 0 }, 0, messages)
}