///|
pub struct ScenarioPoint {
  flow : Double
  target_temperature : Double
  duty : Double
  area_m2 : Double
  quality_score : Double
} derive(Debug)

///|
pub struct ScenarioMatrix {
  points : Array[ScenarioPoint]
  minimum_duty : Double
  maximum_duty : Double
  minimum_area : Double
  maximum_area : Double
} derive(Debug)

///|
pub fn evaluate_scenario_point(
  inlet_temperature : Double,
  flow : Double,
  target_temperature : Double,
) -> ScenarioPoint {
  let stream = process_water_stream(
    id="matrix-feed",
    molar_flow=flow,
    temperature=inlet_temperature,
  )
  let duty = heat_duty(stream, stream.with_temperature(target_temperature))
  let sizing = size_heater(duty, 30.0, 500.0, 0.85)
  let quality = energy_balance_quality(duty, duty, 1.0e-6)
  {
    flow,
    target_temperature,
    duty,
    area_m2: sizing.area_m2,
    quality_score: quality_score(quality),
  }
}

///|
pub fn evaluate_scenario_matrix(
  inlet_temperature : Double,
  flows : Array[Double],
  targets : Array[Double],
) -> ScenarioMatrix {
  let points : Array[ScenarioPoint] = []
  for flow in flows {
    for target in targets {
      points.push(evaluate_scenario_point(inlet_temperature, flow, target))
    }
  }
  if points.length() == 0 {
    {
      points,
      minimum_duty: 0.0,
      maximum_duty: 0.0,
      minimum_area: 0.0,
      maximum_area: 0.0,
    }
  } else {
    let mut min_duty = points[0].duty
    let mut max_duty = points[0].duty
    let mut min_area = points[0].area_m2
    let mut max_area = points[0].area_m2
    for point in points {
      min_duty = min_duty.min(point.duty)
      max_duty = max_duty.max(point.duty)
      min_area = min_area.min(point.area_m2)
      max_area = max_area.max(point.area_m2)
    }
    {
      points,
      minimum_duty: min_duty,
      maximum_duty: max_duty,
      minimum_area: min_area,
      maximum_area: max_area,
    }
  }
}

///|
pub fn scenario_matrix_find_max_duty(matrix : ScenarioMatrix) -> ScenarioPoint? {
  if matrix.points.length() == 0 {
    None
  } else {
    let mut best = matrix.points[0]
    for point in matrix.points {
      if point.duty > best.duty {
        best = point
      }
    }
    Some(best)
  }
}

///|
pub fn scenario_matrix_find_min_area(matrix : ScenarioMatrix) -> ScenarioPoint? {
  if matrix.points.length() == 0 {
    None
  } else {
    let mut best = matrix.points[0]
    for point in matrix.points {
      if point.area_m2 < best.area_m2 {
        best = point
      }
    }
    Some(best)
  }
}

///|
pub fn scenario_matrix_csv_header() -> String {
  "flow,target_temperature,duty,area_m2,quality_score"
}

///|
pub fn scenario_matrix_csv_row(point : ScenarioPoint) -> String {
  "\{point.flow},\{point.target_temperature},\{point.duty},\{point.area_m2},\{point.quality_score}"
}

///|
pub fn scenario_matrix_csv(matrix : ScenarioMatrix) -> String {
  let mut output = scenario_matrix_csv_header() + "\n"
  for point in matrix.points {
    output = output + scenario_matrix_csv_row(point) + "\n"
  }
  output
}

///|
pub fn scenario_matrix_point_count(matrix : ScenarioMatrix) -> Int {
  matrix.points.length()
}

///|
pub fn scenario_matrix_average_quality(matrix : ScenarioMatrix) -> Double {
  if matrix.points.length() == 0 {
    0.0
  } else {
    let mut total = 0.0
    for point in matrix.points {
      total += point.quality_score
    }
    total / matrix.points.length().to_double()
  }
}

///|
pub fn scenario_matrix_is_monotonic_in_temperature(
  matrix : ScenarioMatrix,
) -> Bool {
  let mut previous = -1.0e30
  for point in matrix.points {
    if point.duty < previous {
      return false
    }
    previous = point.duty
  }
  true
}