///|
pub struct VesselDesign {
  volume : Float
  liquid_volume : Float
  design_pressure : Float
  allowable_pressure : Float
  design_temperature : Float
  allowable_temperature : Float
} derive(Debug, Eq)

///|
pub fn vessel_design(
  volume : Float,
  liquid_volume : Float,
  design_pressure : Float,
  allowable_pressure : Float,
  design_temperature : Float,
  allowable_temperature : Float,
) -> VesselDesign {
  {
    volume,
    liquid_volume,
    design_pressure,
    allowable_pressure,
    design_temperature,
    allowable_temperature,
  }
}

///|
pub fn VesselDesign::volume_margin(self : VesselDesign) -> Float {
  self.volume - self.liquid_volume
}

///|
pub fn VesselDesign::pressure_margin(self : VesselDesign) -> Float {
  self.allowable_pressure - self.design_pressure
}

///|
pub fn VesselDesign::temperature_margin(self : VesselDesign) -> Float {
  self.allowable_temperature - self.design_temperature
}

///|
pub fn VesselDesign::fill_fraction(self : VesselDesign) -> Float {
  if self.volume <= 0.0 {
    0.0
  } else {
    self.liquid_volume / self.volume
  }
}

///|
pub fn VesselDesign::is_acceptable(self : VesselDesign) -> Bool {
  self.volume > 0.0 &&
  self.liquid_volume >= 0.0 &&
  self.volume_margin() >= 0.0 &&
  self.pressure_margin() >= 0.0 &&
  self.temperature_margin() >= 0.0
}

///|
pub fn VesselDesign::recommended_relief_pressure(
  self : VesselDesign,
  factor : Float,
) -> Float {
  self.allowable_pressure * clamp(factor, 0.1, 0.95)
}

///|
pub struct TankDesign {
  diameter : Float
  height : Float
  working_level : Float
  dead_volume : Float
  overflow_level : Float
} derive(Debug, Eq)

///|
pub fn tank_design(
  diameter : Float,
  height : Float,
  working_level : Float,
  dead_volume : Float,
  overflow_level : Float,
) -> TankDesign {
  { diameter, height, working_level, dead_volume, overflow_level }
}

///|
pub fn TankDesign::cross_section(self : TankDesign) -> Float {
  3.141592653589793 * self.diameter * self.diameter / 4.0
}

///|
pub fn TankDesign::working_volume(self : TankDesign) -> Float {
  self.cross_section() * self.working_level
}

///|
pub fn TankDesign::overflow_volume(self : TankDesign) -> Float {
  self.cross_section() * self.overflow_level
}

///|
pub fn TankDesign::usable_volume(self : TankDesign) -> Float {
  self.working_volume() - self.dead_volume
}

///|
pub fn TankDesign::is_acceptable(self : TankDesign) -> Bool {
  self.diameter > 0.0 &&
  self.height > 0.0 &&
  self.working_level >= 0.0 &&
  self.working_level <= self.height &&
  self.overflow_level >= self.working_level
}

///|
pub fn TankDesign::residence_time(
  self : TankDesign,
  flow_rate : Float,
) -> Float {
  residence_time(self.usable_volume(), flow_rate)
}

///|
pub struct ExchangerDesign {
  area : Float
  overall_u : Float
  hot_inlet : Float
  hot_outlet : Float
  cold_inlet : Float
  cold_outlet : Float
  hot_capacity : Float
  cold_capacity : Float
} derive(Debug, Eq)

///|
pub fn exchanger_design(
  area : Float,
  overall_u : Float,
  hot_inlet : Float,
  hot_outlet : Float,
  cold_inlet : Float,
  cold_outlet : Float,
  hot_capacity : Float,
  cold_capacity : Float,
) -> ExchangerDesign {
  {
    area,
    overall_u,
    hot_inlet,
    hot_outlet,
    cold_inlet,
    cold_outlet,
    hot_capacity,
    cold_capacity,
  }
}

///|
pub fn ExchangerDesign::delta_hot(self : ExchangerDesign) -> Float {
  self.hot_inlet - self.hot_outlet
}

///|
pub fn ExchangerDesign::delta_cold(self : ExchangerDesign) -> Float {
  self.cold_outlet - self.cold_inlet
}

///|
pub fn ExchangerDesign::hot_duty(self : ExchangerDesign) -> Float {
  self.hot_capacity * self.delta_hot()
}

///|
pub fn ExchangerDesign::cold_duty(self : ExchangerDesign) -> Float {
  self.cold_capacity * self.delta_cold()
}

///|
pub fn ExchangerDesign::duty_error(self : ExchangerDesign) -> Float {
  normalized_error(self.hot_duty(), self.cold_duty())
}

///|
pub fn ExchangerDesign::lmtd(self : ExchangerDesign) -> Float {
  log_mean_temperature_difference(
    self.hot_inlet - self.cold_outlet,
    self.hot_outlet - self.cold_inlet,
  )
}

///|
pub fn ExchangerDesign::required_area(self : ExchangerDesign) -> Float {
  if self.overall_u <= 0.0 || self.lmtd() <= 0.0 {
    0.0
  } else {
    self.hot_duty() / (self.overall_u * self.lmtd())
  }
}

///|
pub fn ExchangerDesign::area_margin(self : ExchangerDesign) -> Float {
  self.area - self.required_area()
}

///|
pub fn ExchangerDesign::effectiveness(self : ExchangerDesign) -> Float {
  let minimum = if self.hot_capacity < self.cold_capacity {
    self.hot_capacity
  } else {
    self.cold_capacity
  }
  if minimum <= 0.0 || self.hot_inlet <= self.cold_inlet {
    0.0
  } else {
    clamp(
      self.cold_duty() / (minimum * (self.hot_inlet - self.cold_inlet)),
      0.0,
      1.0,
    )
  }
}

///|
pub fn ExchangerDesign::is_acceptable(self : ExchangerDesign) -> Bool {
  self.area > 0.0 &&
  self.overall_u > 0.0 &&
  self.area_margin() >= 0.0 &&
  self.duty_error() <= 0.05
}

///|
pub struct CompressorDesign {
  suction_pressure : Float
  discharge_pressure : Float
  inlet_temperature : Float
  flow_rate : Float
  efficiency : Float
  gas_constant : Float
  heat_capacity_ratio : Float
} derive(Debug, Eq)

///|
pub fn compressor_design(
  suction_pressure : Float,
  discharge_pressure : Float,
  inlet_temperature : Float,
  flow_rate : Float,
  efficiency : Float,
  gas_constant : Float,
  heat_capacity_ratio : Float,
) -> CompressorDesign {
  {
    suction_pressure,
    discharge_pressure,
    inlet_temperature,
    flow_rate,
    efficiency,
    gas_constant,
    heat_capacity_ratio,
  }
}

///|
pub fn CompressorDesign::pressure_ratio(self : CompressorDesign) -> Float {
  if self.suction_pressure <= 0.0 {
    0.0
  } else {
    self.discharge_pressure / self.suction_pressure
  }
}

///|
pub fn CompressorDesign::specific_work(self : CompressorDesign) -> Float {
  if self.efficiency <= 0.0 ||
    self.heat_capacity_ratio <= 1.0 ||
    self.inlet_temperature <= 0.0 {
    0.0
  } else {
    self.gas_constant *
    self.inlet_temperature /
    self.efficiency *
    (self.pressure_ratio().sqrt() - 1.0)
  }
}

///|
pub fn CompressorDesign::power(self : CompressorDesign) -> Float {
  self.flow_rate * self.specific_work()
}

///|
pub fn CompressorDesign::discharge_temperature(
  self : CompressorDesign,
) -> Float {
  if self.inlet_temperature <= 0.0 {
    0.0
  } else {
    self.inlet_temperature * self.pressure_ratio().sqrt()
  }
}

///|
pub fn CompressorDesign::is_acceptable(
  self : CompressorDesign,
  maximum_temperature : Float,
) -> Bool {
  self.pressure_ratio() >= 1.0 &&
  self.efficiency > 0.0 &&
  self.efficiency <= 1.0 &&
  self.discharge_temperature() <= maximum_temperature
}

///|
pub struct ValveDesign {
  flow_coefficient : Float
  pressure_drop : Float
  density : Float
  opening : Float
  maximum_flow : Float
} derive(Debug, Eq)

///|
pub fn valve_design(
  flow_coefficient : Float,
  pressure_drop : Float,
  density : Float,
  opening : Float,
  maximum_flow : Float,
) -> ValveDesign {
  { flow_coefficient, pressure_drop, density, opening, maximum_flow }
}

///|
pub fn ValveDesign::estimated_flow(self : ValveDesign) -> Float {
  if self.density <= 0.0 || self.pressure_drop <= 0.0 {
    0.0
  } else {
    self.flow_coefficient *
    self.opening *
    (self.pressure_drop / self.density).sqrt()
  }
}

///|
pub fn ValveDesign::capacity_margin(self : ValveDesign) -> Float {
  self.maximum_flow - self.estimated_flow()
}

///|
pub fn ValveDesign::is_acceptable(self : ValveDesign) -> Bool {
  self.flow_coefficient > 0.0 &&
  self.opening >= 0.0 &&
  self.opening <= 1.0 &&
  self.capacity_margin() >= 0.0
}

///|
pub struct PipeDesign {
  length : Float
  diameter : Float
  roughness : Float
  flow_rate : Float
  density : Float
  viscosity : Float
  allowable_drop : Float
} derive(Debug, Eq)

///|
pub fn pipe_design(
  length : Float,
  diameter : Float,
  roughness : Float,
  flow_rate : Float,
  density : Float,
  viscosity : Float,
  allowable_drop : Float,
) -> PipeDesign {
  { length, diameter, roughness, flow_rate, density, viscosity, allowable_drop }
}

///|
pub fn PipeDesign::area(self : PipeDesign) -> Float {
  3.141592653589793 * self.diameter * self.diameter / 4.0
}

///|
pub fn PipeDesign::velocity(self : PipeDesign) -> Float {
  if self.area() <= 0.0 {
    0.0
  } else {
    self.flow_rate / self.area()
  }
}

///|
pub fn PipeDesign::reynolds(self : PipeDesign) -> Float {
  reynolds_number(self.density, self.velocity(), self.diameter, self.viscosity)
}

///|
pub fn PipeDesign::friction_factor(self : PipeDesign) -> Float {
  pressure_drop_case(
    self.length,
    self.diameter,
    self.velocity(),
    self.density,
    self.viscosity,
    self.roughness,
    0.0,
  ).friction_factor()
}

///|
pub fn PipeDesign::pressure_drop(self : PipeDesign) -> Float {
  pressure_drop_case(
    self.length,
    self.diameter,
    self.velocity(),
    self.density,
    self.viscosity,
    self.roughness,
    0.0,
  ).major_loss()
}

///|
pub fn PipeDesign::pressure_margin(self : PipeDesign) -> Float {
  self.allowable_drop - self.pressure_drop()
}

///|
pub fn PipeDesign::is_acceptable(self : PipeDesign) -> Bool {
  self.length > 0.0 &&
  self.diameter > 0.0 &&
  self.density > 0.0 &&
  self.viscosity > 0.0 &&
  self.pressure_margin() >= 0.0
}

///|
pub struct UtilityLoad {
  name : String
  demand : Float
  availability : Float
  priority : Int
} derive(Debug, Eq)

///|
pub fn utility_load(
  name : String,
  demand : Float,
  availability : Float,
  priority : Int,
) -> UtilityLoad {
  { name, demand, availability, priority }
}

///|
pub fn UtilityLoad::margin(self : UtilityLoad) -> Float {
  self.availability - self.demand
}

///|
pub fn UtilityLoad::is_supplied(self : UtilityLoad) -> Bool {
  self.demand >= 0.0 && self.margin() >= 0.0
}

///|
pub fn UtilityLoad::utilization(self : UtilityLoad) -> Float {
  if self.availability <= 0.0 {
    0.0
  } else {
    self.demand / self.availability
  }
}

///|
pub fn sort_loads_by_priority(loads : Array[UtilityLoad]) -> Array[UtilityLoad] {
  let result = loads.copy()
  result.sort_by(fn(left, right) { left.priority - right.priority })
  result
}

///|
pub fn total_utility_demand(loads : Array[UtilityLoad]) -> Float {
  let mut total : Float = 0.0
  for load in loads {
    total = total + load.demand
  }
  total
}

///|
pub fn total_utility_availability(loads : Array[UtilityLoad]) -> Float {
  let mut total : Float = 0.0
  for load in loads {
    total = total + load.availability
  }
  total
}

///|
pub fn utility_margin(loads : Array[UtilityLoad]) -> Float {
  total_utility_availability(loads) - total_utility_demand(loads)
}

///|
pub fn utility_load_table(loads : Array[UtilityLoad]) -> ReportTable {
  let rows : Array[Array[String]] = []
  for load in loads {
    rows.push([
      load.name,
      "{load.demand}",
      "{load.availability}",
      "{load.margin()}",
      "{load.priority}",
    ])
  }
  table(["utility", "demand", "availability", "margin", "priority"], rows)
}