///|
/// Operating-window validation keeps a calculation inside a declared process envelope.
pub(all) struct OperatingWindow {
  minimum_flow : Double
  maximum_flow : Double
  minimum_concentration : Double
  maximum_concentration : Double
  minimum_distribution : Double
  maximum_distribution : Double
  minimum_recovery : Double
  maximum_recovery : Double
} derive(Debug, Eq, ToJson)

///|
pub fn OperatingWindow::new(
  flow~ : Range,
  concentration~ : Range,
  distribution~ : Range,
  recovery~ : Range,
) -> OperatingWindow {
  {
    minimum_flow: flow.lower,
    maximum_flow: flow.upper,
    minimum_concentration: concentration.lower,
    maximum_concentration: concentration.upper,
    minimum_distribution: distribution.lower,
    maximum_distribution: distribution.upper,
    minimum_recovery: recovery.lower,
    maximum_recovery: recovery.upper,
  }
}

///|
pub(all) struct Range {
  lower : Double
  upper : Double
} derive(Debug, Eq, ToJson)

///|
pub fn Range::new(
  lower~ : Double,
  upper~ : Double,
) -> Range raise @domain.ExtractionError {
  @domain.require_finite("range lower", lower)
  @domain.require_finite("range upper", upper)
  if upper < lower {
    raise @domain.ExtractionError::InvalidArgument(
      message="range upper is below lower",
    )
  }
  { lower, upper }
}

///|
pub fn OperatingWindow::contains(
  self : OperatingWindow,
  flow : Double,
  concentration : Double,
  distribution : Double,
  recovery : Double,
) -> Bool {
  flow >= self.minimum_flow &&
  flow <= self.maximum_flow &&
  concentration >= self.minimum_concentration &&
  concentration <= self.maximum_concentration &&
  distribution >= self.minimum_distribution &&
  distribution <= self.maximum_distribution &&
  recovery >= self.minimum_recovery &&
  recovery <= self.maximum_recovery
}

///|
pub(all) enum Violation {
  BelowMinimum(name~ : String, value~ : Double, minimum~ : Double)
  AboveMaximum(name~ : String, value~ : Double, maximum~ : Double)
} derive(Debug, Eq, ToJson)

///|
pub fn OperatingWindow::validate(
  self : OperatingWindow,
  flow : Double,
  concentration : Double,
  distribution : Double,
  recovery : Double,
) -> Array[Violation] {
  let violations = []
  check(violations, "flow", flow, self.minimum_flow, self.maximum_flow)
  check(
    violations,
    "concentration",
    concentration,
    self.minimum_concentration,
    self.maximum_concentration,
  )
  check(
    violations,
    "distribution",
    distribution,
    self.minimum_distribution,
    self.maximum_distribution,
  )
  check(
    violations,
    "recovery",
    recovery,
    self.minimum_recovery,
    self.maximum_recovery,
  )
  violations
}

///|
fn check(
  violations : Array[Violation],
  name : String,
  value : Double,
  lower : Double,
  upper : Double,
) -> Unit {
  if value < lower {
    violations.push(Violation::BelowMinimum(name~, value~, minimum=lower))
  }
  if value > upper {
    violations.push(Violation::AboveMaximum(name~, value~, maximum=upper))
  }
}

///|
pub(all) struct LedgerEntry {
  name : String
  inlet : Double
  outlet : Double
  generated : Double
  consumed : Double
  residual : Double
} derive(Debug, Eq, ToJson)

///|
pub fn ledger_entry(
  name~ : String,
  inlet~ : Double,
  outlet~ : Double,
  generated? : Double = 0.0,
  consumed? : Double = 0.0,
) -> LedgerEntry raise @domain.ExtractionError {
  @domain.require_finite("ledger inlet", inlet)
  @domain.require_finite("ledger outlet", outlet)
  @domain.require_non_negative("ledger generated", generated)
  @domain.require_non_negative("ledger consumed", consumed)
  {
    name,
    inlet,
    outlet,
    generated,
    consumed,
    residual: inlet + generated - consumed - outlet,
  }
}

///|
pub fn ledger_passes(
  entries : ArrayView[LedgerEntry],
  tolerance~ : Double,
) -> Bool {
  for entry in entries {
    if entry.residual.abs() > tolerance {
      return false
    }
  }
  true
}

///|
pub fn maximum_residual(entries : ArrayView[LedgerEntry]) -> Double {
  let mut maximum = 0.0
  for entry in entries {
    maximum = maximum.max(entry.residual.abs())
  }
  maximum
}

///|
pub(all) struct SafetyFactor {
  nominal : Double
  factor : Double
  design : Double
} derive(Debug, Eq, ToJson)

///|
pub fn apply_safety_factor(
  nominal~ : Double,
  factor~ : Double,
) -> SafetyFactor raise @domain.ExtractionError {
  @domain.require_non_negative("nominal quantity", nominal)
  @domain.require_positive("safety factor", factor)
  { nominal, factor, design: nominal * factor }
}

///|
pub fn recommended_safety_factor(
  uncertainty_fraction : Double,
  minimum? : Double = 1.05,
) -> Double raise @domain.ExtractionError {
  @domain.require_non_negative("uncertainty fraction", uncertainty_fraction)
  @domain.require_positive("minimum safety factor", minimum)
  minimum.max(1.0 + 2.0 * uncertainty_fraction)
}