///|
/// Equipment categories used for preliminary process sizing.
pub(all) enum EquipmentKind {
  Mixer
  Settler
  ExtractorColumn
  HeatExchanger
  Pump
  StorageTank
} derive(Debug, Eq, ToJson)

///|
pub(all) struct EquipmentSpec {
  name : String
  kind : EquipmentKind
  design_flow : Double
  design_residence_time : Double
  design_pressure : Double
  design_temperature : Double
  safety_factor : Double
} derive(Debug, Eq, ToJson)

///|
pub fn EquipmentSpec::new(
  name~ : String,
  kind~ : EquipmentKind,
  design_flow~ : Double,
  design_residence_time~ : Double,
  design_pressure~ : Double,
  design_temperature~ : Double,
  safety_factor? : Double = 1.15,
) -> EquipmentSpec raise @domain.ExtractionError {
  if name.trim().length() == 0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="equipment name cannot be blank",
    )
  }
  @domain.require_positive("equipment design flow", design_flow)
  @domain.require_positive("equipment residence time", design_residence_time)
  @domain.require_positive("equipment design pressure", design_pressure)
  @domain.require_positive("equipment design temperature", design_temperature)
  @domain.require_positive("equipment safety factor", safety_factor)
  {
    name,
    kind,
    design_flow,
    design_residence_time,
    design_pressure,
    design_temperature,
    safety_factor,
  }
}

///|
pub(all) struct EquipmentSizing {
  name : String
  kind : EquipmentKind
  working_volume : Double
  design_volume : Double
  cross_section_area : Double
  diameter : Double
  heat_transfer_area : Double
  pump_head : Double
  warnings : Array[String]
} derive(Debug, Eq, ToJson)

///|
pub fn size_equipment(
  spec : EquipmentSpec,
  density~ : Double,
  velocity~ : Double,
  heat_duty~ : Double,
  heat_transfer_coefficient~ : Double,
) -> EquipmentSizing raise @domain.ExtractionError {
  @domain.require_positive("equipment density", density)
  @domain.require_positive("equipment velocity", velocity)
  @domain.require_non_negative("equipment heat duty", heat_duty)
  @domain.require_positive(
    "equipment heat-transfer coefficient", heat_transfer_coefficient,
  )
  let working_volume = spec.design_flow * spec.design_residence_time
  let design_volume = working_volume * spec.safety_factor
  let area = spec.design_flow / velocity
  let diameter = (4.0 * area / 3.141592653589793).sqrt()
  let heat_area = if heat_duty <= 0.0 {
    0.0
  } else {
    heat_duty / heat_transfer_coefficient
  }
  let head = spec.design_pressure / (density * 9.80665)
  let warnings : Array[String] = []
  if diameter > 10.0 {
    warnings.push("diameter exceeds preliminary equipment range")
  }
  if spec.design_residence_time < 1.0 {
    warnings.push("residence time is below one second")
  }
  if heat_area > design_volume * 100.0 {
    warnings.push("heat-transfer area is large relative to vessel volume")
  }
  {
    name: spec.name,
    kind: spec.kind,
    working_volume,
    design_volume,
    cross_section_area: area,
    diameter,
    heat_transfer_area: heat_area,
    pump_head: head,
    warnings,
  }
}

///|
pub(all) struct SettlerSizing {
  volume : Double
  interface_area : Double
  length : Double
  width : Double
  height : Double
  overflow_rate : Double
} derive(Debug, Eq, ToJson)

///|
pub fn size_settler(
  total_flow~ : Double,
  residence_time~ : Double,
  length_to_width? : Double = 3.0,
  width_to_height? : Double = 2.0,
) -> SettlerSizing raise @domain.ExtractionError {
  @domain.require_positive("settler flow", total_flow)
  @domain.require_positive("settler residence time", residence_time)
  @domain.require_positive("settler length-to-width", length_to_width)
  @domain.require_positive("settler width-to-height", width_to_height)
  let volume = total_flow * residence_time
  let height = @math.pow(
    volume / (length_to_width * width_to_height),
    1.0 / 3.0,
  )
  let width = height * width_to_height
  let length = width * length_to_width
  {
    volume,
    interface_area: length * width,
    length,
    width,
    height,
    overflow_rate: total_flow / (length * width),
  }
}

///|
pub(all) struct MixerSizing {
  volume : Double
  power : Double
  power_per_volume : Double
  tip_speed : Double
  torque : Double
} derive(Debug, Eq, ToJson)

///|
pub fn size_mixer(
  flow~ : Double,
  residence_time~ : Double,
  density~ : Double,
  power_number~ : Double,
  rotation_per_second~ : Double,
  impeller_diameter~ : Double,
) -> MixerSizing raise @domain.ExtractionError {
  @domain.require_positive("mixer flow", flow)
  @domain.require_positive("mixer residence time", residence_time)
  @domain.require_positive("mixer density", density)
  @domain.require_non_negative("mixer power number", power_number)
  @domain.require_positive("mixer rotation", rotation_per_second)
  @domain.require_positive("impeller diameter", impeller_diameter)
  let volume = flow * residence_time
  let power = power_number *
    density *
    rotation_per_second *
    rotation_per_second *
    rotation_per_second *
    impeller_diameter *
    impeller_diameter *
    impeller_diameter *
    impeller_diameter *
    impeller_diameter
  let tip = 3.141592653589793 * rotation_per_second * impeller_diameter
  {
    volume,
    power,
    power_per_volume: power / volume,
    tip_speed: tip,
    torque: power / (2.0 * 3.141592653589793 * rotation_per_second),
  }
}

///|
pub(all) struct HeatExchangerSizing {
  area : Double
  log_mean_temperature_difference : Double
  duty : Double
  hot_outlet_temperature : Double
  cold_outlet_temperature : Double
  pinch_temperature_difference : Double
} derive(Debug, Eq, ToJson)

///|
pub fn size_heat_exchanger(
  duty~ : Double,
  overall_coefficient~ : Double,
  hot_inlet_temperature~ : Double,
  hot_outlet_temperature~ : Double,
  cold_inlet_temperature~ : Double,
  cold_outlet_temperature~ : Double,
) -> HeatExchangerSizing raise @domain.ExtractionError {
  @domain.require_positive("heat exchanger duty", duty)
  @domain.require_positive("overall coefficient", overall_coefficient)
  let delta_one = hot_inlet_temperature - cold_outlet_temperature
  let delta_two = hot_outlet_temperature - cold_inlet_temperature
  if delta_one <= 0.0 || delta_two <= 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="heat exchanger temperature cross",
    )
  }
  let lmtd = if (delta_one - delta_two).abs() <= 1.0e-12 {
    delta_one
  } else {
    (delta_one - delta_two) / approximate_log(delta_one / delta_two)
  }
  {
    area: duty / (overall_coefficient * lmtd),
    log_mean_temperature_difference: lmtd,
    duty,
    hot_outlet_temperature,
    cold_outlet_temperature,
    pinch_temperature_difference: delta_one.min(delta_two),
  }
}

///|
pub(all) struct PumpSizing {
  head : Double
  hydraulic_power : Double
  shaft_power : Double
  efficiency : Double
  pressure_rise : Double
} derive(Debug, Eq, ToJson)

///|
pub fn size_pump(
  flow~ : Double,
  pressure_rise~ : Double,
  density~ : Double,
  efficiency~ : Double,
) -> PumpSizing raise @domain.ExtractionError {
  @domain.require_positive("pump flow", flow)
  @domain.require_positive("pressure rise", pressure_rise)
  @domain.require_positive("pump density", density)
  @domain.require_fraction("pump efficiency", efficiency)
  if efficiency <= 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="pump efficiency must be positive",
    )
  }
  let hydraulic = flow * pressure_rise
  let shaft = hydraulic / efficiency
  {
    head: pressure_rise / (density * 9.80665),
    hydraulic_power: hydraulic,
    shaft_power: shaft,
    efficiency,
    pressure_rise,
  }
}

///|
pub fn equipment_utilization(
  actual_flow~ : Double,
  design_flow~ : Double,
) -> Double raise @domain.ExtractionError {
  @domain.require_non_negative("actual equipment flow", actual_flow)
  @domain.require_positive("design equipment flow", design_flow)
  actual_flow / design_flow
}

///|
pub fn equipment_margin(
  actual_flow~ : Double,
  design_flow~ : Double,
) -> Double raise @domain.ExtractionError {
  1.0 - equipment_utilization(actual_flow~, design_flow~)
}

///|
pub fn vessel_volume_for_batch(
  batch_mass~ : Double,
  density~ : @units.Density,
  fill_fraction~ : Double,
  safety_factor? : Double = 1.15,
) -> @units.Volume raise @domain.ExtractionError {
  @domain.require_positive("batch mass", batch_mass)
  @domain.require_fraction("fill fraction", fill_fraction)
  @domain.require_positive("vessel safety factor", safety_factor)
  if fill_fraction <= 0.0 {
    raise @domain.ExtractionError::InvalidArgument(
      message="fill fraction must be positive",
    )
  }
  @units.Volume::new(
    value=batch_mass /
      density.mass_kg_per_cubic_meter /
      fill_fraction *
      safety_factor,
    unit=@units.VolumeUnit::CubicMeter,
  )
}

///|
fn approximate_log(value : Double) -> Double {
  let mut scaled = value
  let mut exponent = 0.0
  while scaled > 2.0 {
    scaled /= 2.0
    exponent += 1.0
  }
  while scaled < 0.5 {
    scaled *= 2.0
    exponent -= 1.0
  }
  let z = (scaled - 1.0) / (scaled + 1.0)
  let z_squared = z * z
  let mut power = z
  let mut result = 0.0
  for index in 0..<18 {
    result += power / (2.0 * index.to_double() + 1.0)
    power *= z_squared
  }
  2.0 * result + exponent * 0.6931471805599453
}