///|
pub enum StreamDirection {
  Inlet
  Outlet
} derive(Debug, Eq)

///|
pub fn stream_directions() -> Array[StreamDirection] {
  [Inlet, Outlet]
}

///|
pub fn inlet() -> StreamDirection {
  Inlet
}

///|
pub fn outlet() -> StreamDirection {
  Outlet
}

///|
pub struct ComponentFlow {
  component : String
  value : Float
  unit : MeasureUnit
} derive(Debug, Eq)

///|
pub fn component_flow(
  component : String,
  value : Float,
  unit : MeasureUnit,
) -> ComponentFlow {
  { component, value, unit }
}

///|
pub struct ProcessStream {
  name : String
  direction : StreamDirection
  components : Array[ComponentFlow]
} derive(Debug, Eq)

///|
pub fn stream(
  name : String,
  direction : StreamDirection,
  components : Array[ComponentFlow],
) -> ProcessStream {
  { name, direction, components }
}

///|
pub struct ComponentBalance {
  component : String
  inlet : Float
  outlet : Float
  net : Float
  relative_error : Float
} derive(Debug, Eq)

///|
pub struct MaterialBalance {
  components : Array[ComponentBalance]
  total_inlet : Float
  total_outlet : Float
} derive(Debug, Eq)

///|
fn find_component(names : Array[String], name : String) -> Int? {
  for index, item in names {
    if item == name {
      return Some(index)
    }
  }
  None
}

///|
pub fn material_balance(streams : Array[ProcessStream]) -> MaterialBalance {
  let names : Array[String] = []
  let inlet : Array[Float] = []
  let outlet : Array[Float] = []
  let mut total_inlet : Float = 0.0
  let mut total_outlet : Float = 0.0
  for item in streams {
    for flow in item.components {
      if item.direction is Inlet {
        match find_component(names, flow.component) {
          Some(index) => inlet[index] = inlet[index] + flow.value
          None => {
            names.push(flow.component)
            inlet.push(flow.value)
            outlet.push(0.0)
          }
        }
        total_inlet = total_inlet + flow.value
      } else {
        match find_component(names, flow.component) {
          Some(index) => outlet[index] = outlet[index] + flow.value
          None => {
            names.push(flow.component)
            inlet.push(0.0)
            outlet.push(flow.value)
          }
        }
        total_outlet = total_outlet + flow.value
      }
    }
  }
  let components : Array[ComponentBalance] = []
  for index, name in names {
    let input = inlet[index]
    let output = outlet[index]
    let denominator = if input.abs() > output.abs() {
      input.abs()
    } else {
      output.abs()
    }
    let relative_error : Float = if denominator == 0.0 {
      0.0
    } else {
      (input - output).abs() / denominator
    }
    components.push({
      component: name,
      inlet: input,
      outlet: output,
      net: input - output,
      relative_error,
    })
  }
  { components, total_inlet, total_outlet }
}

///|
pub fn MaterialBalance::is_closed(
  self : MaterialBalance,
  tolerance? : Float = 0.001,
) -> Bool {
  for item in self.components {
    if item.relative_error > tolerance {
      return false
    }
  }
  true
}

///|
pub fn MaterialBalance::limiting_component(self : MaterialBalance) -> String? {
  if self.components.length() == 0 {
    return None
  }
  let mut best = self.components[0]
  for item in self.components[1:] {
    if item.relative_error > best.relative_error {
      best = item
    }
  }
  Some(best.component)
}

///|
pub fn MaterialBalance::component(
  self : MaterialBalance,
  name : String,
) -> ComponentBalance? {
  for item in self.components {
    if item.component == name {
      return Some(item)
    }
  }
  None
}

///|
pub fn MaterialBalance::closure_percent(self : MaterialBalance) -> Float {
  if self.total_inlet == 0.0 {
    if self.total_outlet == 0.0 {
      100.0
    } else {
      0.0
    }
  } else {
    100.0 * self.total_outlet / self.total_inlet
  }
}

///|
pub fn MaterialBalance::to_markdown(self : MaterialBalance) -> String {
  let lines : Array[String] = [
    "| Component | Inlet | Outlet | Net | Relative error |", "| --- | ---: | ---: | ---: | ---: |",
  ]
  for item in self.components {
    lines.push(
      "| \{item.component} | \{item.inlet} | \{item.outlet} | \{item.net} | \{item.relative_error} |",
    )
  }
  lines.push("")
  lines.push("Closure: \{self.closure_percent()}%")
  lines.join("\n")
}

///|
pub struct EnergyStream {
  name : String
  duty : Quantity
  direction : StreamDirection
} derive(Debug, Eq)

///|
pub fn energy_stream(
  name : String,
  duty : Quantity,
  direction : StreamDirection,
) -> EnergyStream {
  { name, duty, direction }
}

///|
pub struct EnergyBalance {
  inlet : Float
  outlet : Float
  residual : Float
  streams : Array[EnergyStream]
} derive(Debug, Eq)

///|
pub fn energy_balance(streams : Array[EnergyStream]) -> EnergyBalance {
  let mut inlet : Float = 0.0
  let mut outlet : Float = 0.0
  for item in streams {
    match item.duty.convert_to(KiloJoule) {
      Ok(value) =>
        if item.direction is Inlet {
          inlet = inlet + value
        } else {
          outlet = outlet + value
        }
      Err(_) => ()
    }
  }
  { inlet, outlet, residual: inlet - outlet, streams }
}

///|
pub fn EnergyBalance::is_closed(
  self : EnergyBalance,
  tolerance? : Float = 0.001,
) -> Bool {
  self.residual.abs() <= tolerance
}

///|
pub fn EnergyBalance::specific_duty(
  self : EnergyBalance,
  mass : Quantity,
) -> Result[Quantity, QuantityError] {
  match mass.convert_to(Kilogram) {
    Ok(value) => Ok(quantity(self.residual / value, KiloJoule))
    Err(error) => Err(error)
  }
}