///|
pub(all) struct StreamStatistics {
  stream : String
  total_flow : Double
  solute_flow : Double
  solute_fraction : Double
  concentration_ratio : Double
  thermal_content : Double
  pressure_energy : Double
} derive(Debug, Eq, ToJson)

///|
pub(all) struct StreamComparison {
  carrier_flow_change : Double
  solvent_flow_change : Double
  solute_change : Double
  temperature_change : Double
  pressure_change : Double
  concentration_change : Double
} derive(Debug, Eq, ToJson)

///|
pub fn stream_statistics(
  stream : ProcessStream,
  heat_capacity : Double,
) -> StreamStatistics {
  let total = stream.total_flow()
  let solute = stream.total_solute()
  {
    stream: stream.name,
    total_flow: total,
    solute_flow: solute,
    solute_fraction: if total == 0.0 {
      0.0
    } else {
      solute / total
    },
    concentration_ratio: stream_concentration_ratio(stream),
    thermal_content: total * heat_capacity * stream.temperature.kelvin(),
    pressure_energy: total * stream.pressure.pascal(),
  }
}

///|
pub fn compare_streams(
  first : ProcessStream,
  second : ProcessStream,
) -> StreamComparison {
  {
    carrier_flow_change: second.carrier_flow - first.carrier_flow,
    solvent_flow_change: second.solvent_flow - first.solvent_flow,
    solute_change: second.total_solute() - first.total_solute(),
    temperature_change: second.temperature.kelvin() - first.temperature.kelvin(),
    pressure_change: second.pressure.pascal() - first.pressure.pascal(),
    concentration_change: second.carrier_concentration -
    first.carrier_concentration,
  }
}

///|
pub fn stream_change_magnitude(change : StreamComparison) -> Double {
  (change.carrier_flow_change * change.carrier_flow_change +
  change.solvent_flow_change * change.solvent_flow_change +
  change.solute_change * change.solute_change +
  change.temperature_change * change.temperature_change +
  change.pressure_change * change.pressure_change +
  change.concentration_change * change.concentration_change).sqrt()
}

///|
pub fn stream_is_dilute(stream : ProcessStream, threshold : Double) -> Bool {
  stream_concentration_ratio(stream) <= threshold
}

///|
pub fn stream_is_rich(stream : ProcessStream, threshold : Double) -> Bool {
  stream_concentration_ratio(stream) >= threshold
}

///|
pub fn stream_solvent_fraction(stream : ProcessStream) -> Double {
  if stream.total_flow() == 0.0 {
    0.0
  } else {
    stream.solvent_flow / stream.total_flow()
  }
}

///|
pub fn stream_carrier_fraction(stream : ProcessStream) -> Double {
  if stream.total_flow() == 0.0 {
    0.0
  } else {
    stream.carrier_flow / stream.total_flow()
  }
}

///|
pub fn stream_flow_ratio(
  first : ProcessStream,
  second : ProcessStream,
) -> Double {
  if second.total_flow() == 0.0 {
    0.0
  } else {
    first.total_flow() / second.total_flow()
  }
}

///|
pub fn stream_solute_ratio(
  first : ProcessStream,
  second : ProcessStream,
) -> Double {
  if second.total_solute() == 0.0 {
    0.0
  } else {
    first.total_solute() / second.total_solute()
  }
}

///|
pub fn stream_temperature_ratio(
  first : ProcessStream,
  second : ProcessStream,
) -> Double {
  if second.temperature.kelvin() == 0.0 {
    0.0
  } else {
    first.temperature.kelvin() / second.temperature.kelvin()
  }
}

///|
pub fn stream_pressure_ratio(
  first : ProcessStream,
  second : ProcessStream,
) -> Double {
  if second.pressure.pascal() == 0.0 {
    0.0
  } else {
    first.pressure.pascal() / second.pressure.pascal()
  }
}

///|
pub fn stream_energy_change(
  first : ProcessStream,
  second : ProcessStream,
  heat_capacity : Double,
) -> Double {
  second.total_flow() * heat_capacity * second.temperature.kelvin() -
  first.total_flow() * heat_capacity * first.temperature.kelvin()
}

///|
pub fn stream_pressure_work(
  first : ProcessStream,
  second : ProcessStream,
) -> Double {
  second.total_flow() * second.pressure.pascal() -
  first.total_flow() * first.pressure.pascal()
}

///|
pub fn stream_quality_index(stream : ProcessStream) -> Double {
  let solute_fraction = stream_solute_fraction(stream)
  let solvent_fraction = stream_solvent_fraction(stream)
  (1.0 - solute_fraction) * 0.5 + solvent_fraction * 0.5
}

///|
pub fn stream_recovery_factor(
  feed : ProcessStream,
  product : ProcessStream,
) -> Double {
  if feed.total_solute() == 0.0 {
    0.0
  } else {
    product.total_solute() / feed.total_solute()
  }
}

///|
pub fn stream_loss_factor(
  feed : ProcessStream,
  waste : ProcessStream,
) -> Double {
  1.0 - stream_recovery_factor(feed, waste)
}

///|
pub fn stream_concentration_change(
  first : ProcessStream,
  second : ProcessStream,
) -> Double {
  second.carrier_concentration - first.carrier_concentration
}

///|
pub fn stream_solvent_concentration_change(
  first : ProcessStream,
  second : ProcessStream,
) -> Double {
  second.solvent_concentration - first.solvent_concentration
}

///|
pub fn stream_temperature_change(
  first : ProcessStream,
  second : ProcessStream,
) -> Double {
  second.temperature.kelvin() - first.temperature.kelvin()
}

///|
pub fn stream_pressure_change(
  first : ProcessStream,
  second : ProcessStream,
) -> Double {
  second.pressure.pascal() - first.pressure.pascal()
}

///|
pub fn streams_balanced(
  inlet : ProcessStream,
  outlet : ProcessStream,
  tolerance : Double,
) -> Bool {
  compare_streams(inlet, outlet).solute_change.abs() <= tolerance &&
  compare_streams(inlet, outlet).carrier_flow_change.abs() <= tolerance &&
  compare_streams(inlet, outlet).solvent_flow_change.abs() <= tolerance
}

///|
pub fn stream_summary_line(statistics : StreamStatistics) -> String {
  statistics.stream +
  ": flow=" +
  statistics.total_flow.to_string() +
  ",solute=" +
  statistics.solute_flow.to_string() +
  ",fraction=" +
  statistics.solute_fraction.to_string() +
  ",ratio=" +
  statistics.concentration_ratio.to_string()
}

///|
pub fn stream_statistics_csv(
  statistics : ArrayView[StreamStatistics],
) -> String {
  let lines = [
    "stream,total_flow,solute_flow,solute_fraction,concentration_ratio,thermal_content,pressure_energy",
  ]
  for statistic in statistics {
    lines.push(
      statistic.stream +
      "," +
      statistic.total_flow.to_string() +
      "," +
      statistic.solute_flow.to_string() +
      "," +
      statistic.solute_fraction.to_string() +
      "," +
      statistic.concentration_ratio.to_string() +
      "," +
      statistic.thermal_content.to_string() +
      "," +
      statistic.pressure_energy.to_string(),
    )
  }
  lines.join("\n")
}

///|
pub fn stream_statistics_markdown(
  statistics : ArrayView[StreamStatistics],
) -> String {
  let lines = [
    "| Stream | Total flow | Solute flow | Solute fraction | Concentration ratio | Thermal content | Pressure energy |",
    "|---|---:|---:|---:|---:|---:|---:|",
  ]
  for statistic in statistics {
    lines.push(
      "| " +
      statistic.stream +
      " | " +
      statistic.total_flow.to_string() +
      " | " +
      statistic.solute_flow.to_string() +
      " | " +
      statistic.solute_fraction.to_string() +
      " | " +
      statistic.concentration_ratio.to_string() +
      " | " +
      statistic.thermal_content.to_string() +
      " | " +
      statistic.pressure_energy.to_string() +
      " |",
    )
  }
  lines.join("\n")
}

///|
pub fn stream_comparison_report(comparison : StreamComparison) -> String {
  "carrier_change=" +
  comparison.carrier_flow_change.to_string() +
  ",solvent_change=" +
  comparison.solvent_flow_change.to_string() +
  ",solute_change=" +
  comparison.solute_change.to_string() +
  ",temperature_change=" +
  comparison.temperature_change.to_string() +
  ",pressure_change=" +
  comparison.pressure_change.to_string()
}

///|
pub fn statistics_mean(
  statistics : ArrayView[StreamStatistics],
) -> StreamStatistics {
  if statistics.length() == 0 {
    return {
      stream: "",
      total_flow: 0.0,
      solute_flow: 0.0,
      solute_fraction: 0.0,
      concentration_ratio: 0.0,
      thermal_content: 0.0,
      pressure_energy: 0.0,
    }
  }
  let mut flow = 0.0
  let mut solute = 0.0
  let mut fraction = 0.0
  let mut ratio = 0.0
  let mut thermal = 0.0
  let mut pressure = 0.0
  for statistic in statistics {
    flow += statistic.total_flow
    solute += statistic.solute_flow
    fraction += statistic.solute_fraction
    ratio += statistic.concentration_ratio
    thermal += statistic.thermal_content
    pressure += statistic.pressure_energy
  }
  let count = statistics.length().to_double()
  {
    stream: "mean",
    total_flow: flow / count,
    solute_flow: solute / count,
    solute_fraction: fraction / count,
    concentration_ratio: ratio / count,
    thermal_content: thermal / count,
    pressure_energy: pressure / count,
  }
}

///|
pub fn statistics_total_flow(
  statistics : ArrayView[StreamStatistics],
) -> Double {
  statistics.fold(init=0.0, (sum, statistic) => sum + statistic.total_flow)
}

///|
pub fn statistics_total_solute(
  statistics : ArrayView[StreamStatistics],
) -> Double {
  statistics.fold(init=0.0, (sum, statistic) => sum + statistic.solute_flow)
}

///|
pub fn statistics_maximum_ratio(
  statistics : ArrayView[StreamStatistics],
) -> Double {
  let mut result = 0.0
  for statistic in statistics {
    result = result.max(statistic.concentration_ratio)
  }
  result
}

///|
pub fn statistics_minimum_ratio(
  statistics : ArrayView[StreamStatistics],
) -> Double {
  if statistics.length() == 0 {
    return 0.0
  }
  let mut result = statistics[0].concentration_ratio
  for statistic in statistics {
    result = result.min(statistic.concentration_ratio)
  }
  result
}

///|
pub fn statistics_ratio_range(
  statistics : ArrayView[StreamStatistics],
) -> Double {
  statistics_maximum_ratio(statistics) - statistics_minimum_ratio(statistics)
}

///|
pub fn statistics_filter_rich(
  statistics : ArrayView[StreamStatistics],
  threshold : Double,
) -> Array[StreamStatistics] {
  let result = []
  for statistic in statistics {
    if statistic.concentration_ratio >= threshold {
      result.push(statistic)
    }
  }
  result
}

///|
pub fn statistics_filter_dilute(
  statistics : ArrayView[StreamStatistics],
  threshold : Double,
) -> Array[StreamStatistics] {
  let result = []
  for statistic in statistics {
    if statistic.concentration_ratio <= threshold {
      result.push(statistic)
    }
  }
  result
}

///|
pub fn statistics_quality_range(
  statistics : ArrayView[StreamStatistics],
) -> Double {
  if statistics.length() == 0 {
    return 0.0
  }
  let mut lower = statistics[0].solute_fraction
  let mut upper = lower
  for statistic in statistics {
    lower = lower.min(statistic.solute_fraction)
    upper = upper.max(statistic.solute_fraction)
  }
  upper - lower
}