///|
/// Aggregate metrics for comparing cascade designs.
pub(all) struct CascadeMetrics {
  stage_count : Int
  total_feed_solute : Double
  extracted_solute : Double
  raffinate_solute : Double
  recovery : Double
  average_stage_recovery : Double
  maximum_stage_transfer : Double
  minimum_stage_transfer : Double
  transfer_uniformity : Double
  balance_residual : Double
} derive(Debug, Eq, ToJson)

///|
pub fn cascade_metrics(
  result : CountercurrentResult,
) -> CascadeMetrics raise @domain.ExtractionError {
  if result.stages.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  let first = result.stages[0]
  let last = result.stages[result.stages.length() - 1]
  let feed_solute = first.raffinate_solute_flow
  let extracted = feed_solute - last.raffinate_solute_flow
  let mut maximum = 0.0
  let mut minimum = 1.0e300
  let mut sum = 0.0
  for stage in result.stages {
    let transfer = stage.extract_solute_flow - stage.raffinate_solute_flow
    let positive = transfer.abs()
    maximum = maximum.max(positive)
    minimum = minimum.min(positive)
    sum += positive
  }
  let average = sum / result.stages.length().to_double()
  {
    stage_count: result.stages.length(),
    total_feed_solute: feed_solute,
    extracted_solute: extracted,
    raffinate_solute: last.raffinate_solute_flow,
    recovery: result.recovery,
    average_stage_recovery: if feed_solute <= 0.0 {
      0.0
    } else {
      average / feed_solute
    },
    maximum_stage_transfer: maximum,
    minimum_stage_transfer: minimum,
    transfer_uniformity: if maximum <= 1.0e-12 {
      1.0
    } else {
      minimum / maximum
    },
    balance_residual: result.balance_residual,
  }
}

///|
pub fn nonlinear_cascade_metrics(
  result : NonlinearCountercurrentResult,
) -> CascadeMetrics raise @domain.ExtractionError {
  if result.stages.length() == 0 {
    raise @domain.ExtractionError::InsufficientData(required=1, actual=0)
  }
  let first = result.stages[0]
  let last = result.stages[result.stages.length() - 1]
  let feed_solute = first.raffinate_solute_flow
  let extracted = feed_solute - last.raffinate_solute_flow
  let transfers = [
    for stage in result.stages => {
      (stage.extract_solute_flow - stage.raffinate_solute_flow).abs()
    }
  ]
  let maximum = transfers.fold(init=0.0, (current, value) => current.max(value))
  let minimum = transfers.fold(init=1.0e300, (current, value) => {
    current.min(value)
  })
  {
    stage_count: result.stages.length(),
    total_feed_solute: feed_solute,
    extracted_solute: extracted,
    raffinate_solute: last.raffinate_solute_flow,
    recovery: result.recovery,
    average_stage_recovery: result.recovery / result.stages.length().to_double(),
    maximum_stage_transfer: maximum,
    minimum_stage_transfer: minimum,
    transfer_uniformity: if maximum <= 1.0e-12 {
      1.0
    } else {
      minimum / maximum
    },
    balance_residual: result.balance_residual,
  }
}

///|
pub fn compare_metrics(
  first : CascadeMetrics,
  second : CascadeMetrics,
) -> Array[String] {
  let changes = []
  changes.push("recovery_delta=\{second.recovery - first.recovery}")
  changes.push(
    "solvent_transfer_delta=\{second.extracted_solute - first.extracted_solute}",
  )
  changes.push(
    "uniformity_delta=\{second.transfer_uniformity - first.transfer_uniformity}",
  )
  changes.push(
    "residual_delta=\{second.balance_residual - first.balance_residual}",
  )
  changes
}

///|
pub(all) struct StageRanking {
  stage : Int
  score : Double
  transfer : Double
  rank : Int
} derive(Debug, Eq, ToJson)

///|
pub fn rank_cascade_stages(
  result : CountercurrentResult,
) -> Array[StageRanking] raise @domain.ExtractionError {
  let metrics = cascade_metrics(result)
  let raw = [
    for index, stage in result.stages => {
      let transfer = (stage.extract_solute_flow - stage.raffinate_solute_flow).abs()
      {
        stage: index + 1,
        score: if metrics.maximum_stage_transfer <= 0.0 {
          0.0
        } else {
          transfer / metrics.maximum_stage_transfer
        },
        transfer,
        rank: 0,
      }
    }
  ]
  let ranked : Array[StageRanking] = []
  for item in raw {
    let rank = 1 +
      raw.fold(init=0, (count, other) => {
        if other.score > item.score {
          count + 1
        } else {
          count
        }
      })
    ranked.push({
      stage: item.stage,
      score: item.score,
      transfer: item.transfer,
      rank,
    })
  }
  ranked
}

///|
pub fn cascade_is_balanced(
  result : CountercurrentResult,
  tolerance~ : Double,
) -> Bool raise @domain.ExtractionError {
  @domain.require_positive("cascade balance tolerance", tolerance)
  result.balance_residual.abs() <= tolerance
}