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

///|
pub fn inventory_item(
  component : String,
  flow : Float,
  unit : MeasureUnit,
) -> InventoryItem {
  { component, flow, unit }
}

///|
pub fn InventoryItem::is_valid(self : InventoryItem) -> Bool {
  self.component.trim().length() > 0 && self.flow >= 0.0
}

///|
pub fn InventoryItem::as_kg_per_hour(self : InventoryItem) -> Float {
  match quantity(self.flow, self.unit).convert_to(KgPerHour) {
    Ok(value) => value
    Err(_) => 0.0
  }
}

///|
pub struct StreamInventory {
  name : String
  items : Array[InventoryItem]
} derive(Debug, Eq)

///|
pub fn stream_inventory(
  name : String,
  items : Array[InventoryItem],
) -> StreamInventory {
  { name, items }
}

///|
pub fn StreamInventory::total_flow(self : StreamInventory) -> Float {
  let mut total : Float = 0.0
  for item in self.items {
    total = total + item.flow
  }
  total
}

///|
pub fn StreamInventory::component_flow(
  self : StreamInventory,
  component : String,
) -> Float {
  let mut total : Float = 0.0
  for item in self.items {
    if item.component == component {
      total = total + item.flow
    }
  }
  total
}

///|
pub fn StreamInventory::component_names(
  self : StreamInventory,
) -> Array[String] {
  let names : Array[String] = []
  for item in self.items {
    if !names.contains(item.component) {
      names.push(item.component)
    }
  }
  names
}

///|
pub fn StreamInventory::fraction(
  self : StreamInventory,
  component : String,
) -> Float {
  let total = self.total_flow()
  if total <= 0.0 {
    0.0
  } else {
    self.component_flow(component) / total
  }
}

///|
pub fn StreamInventory::is_empty(self : StreamInventory) -> Bool {
  self.items.length() == 0 || self.total_flow() == 0.0
}

///|
pub fn StreamInventory::with_item(
  self : StreamInventory,
  item : InventoryItem,
) -> StreamInventory {
  let items = self.items.copy()
  items.push(item)
  { ..self, items, }
}

///|
pub fn StreamInventory::without_component(
  self : StreamInventory,
  component : String,
) -> StreamInventory {
  let items : Array[InventoryItem] = []
  for item in self.items {
    if item.component != component {
      items.push(item)
    }
  }
  { ..self, items, }
}

///|
fn inventory_component_index(
  items : Array[InventoryItem],
  component : String,
  unit : MeasureUnit,
) -> Int? {
  for index, item in items {
    if item.component == component && item.unit.dimension() == unit.dimension() {
      return Some(index)
    }
  }
  None
}

///|
fn add_inventory_item(
  items : Array[InventoryItem],
  item : InventoryItem,
) -> Unit {
  match inventory_component_index(items, item.component, item.unit) {
    Some(index) =>
      items[index] = { ..items[index], flow: items[index].flow + item.flow }
    None => items.push(item)
  }
}

///|
pub fn mix_streams(streams : Array[StreamInventory]) -> StreamInventory {
  let items : Array[InventoryItem] = []
  for source in streams {
    for item in source.items {
      add_inventory_item(items, item)
    }
  }
  stream_inventory("mixed", items)
}

///|
pub fn split_stream(
  source : StreamInventory,
  fractions : Array[Float],
) -> Array[StreamInventory] {
  let result : Array[StreamInventory] = []
  for _, fraction in fractions {
    let items : Array[InventoryItem] = []
    let safe_fraction = clamp(fraction, 0.0, 1.0)
    for item in source.items {
      items.push({ ..item, flow: item.flow * safe_fraction })
    }
    result.push(stream_inventory("{source.name}-{index + 1}", items))
  }
  result
}

///|
pub fn split_fractions_are_closed(
  fractions : Array[Float],
  tolerance? : Float = 0.000001,
) -> Bool {
  let mut total : Float = 0.0
  for fraction in fractions {
    total = total + fraction
  }
  (total - 1.0).abs() <= tolerance
}

///|
pub fn StreamInventory::normalize(
  self : StreamInventory,
  target : MeasureUnit,
) -> Result[StreamInventory, QuantityError] {
  let items : Array[InventoryItem] = []
  for item in self.items {
    match quantity(item.flow, item.unit).convert_to(target) {
      Ok(value) => items.push({ ..item, flow: value, unit: target })
      Err(error) => return Err(error)
    }
  }
  Ok({ ..self, items, })
}

///|
pub struct StreamDifference {
  component : String
  left : Float
  right : Float
  difference : Float
  relative_difference : Float
} derive(Debug, Eq)

///|
pub fn compare_streams(
  left : StreamInventory,
  right : StreamInventory,
) -> Array[StreamDifference] {
  let result : Array[StreamDifference] = []
  let names = left.component_names()
  for name in right.component_names() {
    if !names.contains(name) {
      names.push(name)
    }
  }
  for component in names {
    let left_value = left.component_flow(component)
    let right_value = right.component_flow(component)
    let denominator = if left_value.abs() > right_value.abs() {
      left_value.abs()
    } else {
      right_value.abs()
    }
    let relative : Float = if denominator == 0.0 {
      0.0
    } else {
      (left_value - right_value).abs() / denominator
    }
    result.push({
      component,
      left: left_value,
      right: right_value,
      difference: left_value - right_value,
      relative_difference: relative,
    })
  }
  result
}

///|
pub fn stream_difference_table(
  differences : Array[StreamDifference],
) -> ReportTable {
  let rows : Array[Array[String]] = []
  for item in differences {
    rows.push([
      item.component,
      "{item.left}",
      "{item.right}",
      "{item.difference}",
      "{item.relative_difference}",
    ])
  }
  table(["component", "left", "right", "difference", "relative"], rows)
}

///|
pub fn stream_balance_error(
  left : StreamInventory,
  right : StreamInventory,
) -> Float {
  let differences = compare_streams(left, right)
  let mut total : Float = 0.0
  for item in differences {
    total = total + item.difference.abs()
  }
  total
}

///|
pub fn StreamInventory::mass_fraction(
  self : StreamInventory,
  component : String,
) -> Float {
  self.fraction(component)
}

///|
pub fn StreamInventory::component_count(self : StreamInventory) -> Int {
  self.component_names().length()
}

///|
pub fn StreamInventory::largest_component(self : StreamInventory) -> String? {
  if self.items.length() == 0 {
    None
  } else {
    let mut best = self.items[0]
    for item in self.items[1:] {
      if item.flow > best.flow {
        best = item
      }
    }
    Some(best.component)
  }
}

///|
pub fn StreamInventory::smallest_component(self : StreamInventory) -> String? {
  if self.items.length() == 0 {
    None
  } else {
    let mut best = self.items[0]
    for item in self.items[1:] {
      if item.flow < best.flow {
        best = item
      }
    }
    Some(best.component)
  }
}

///|
pub fn StreamInventory::to_table(self : StreamInventory) -> ReportTable {
  let rows : Array[Array[String]] = []
  for item in self.items {
    rows.push([
      item.component,
      "{item.flow}",
      item.unit.symbol(),
      "{self.fraction(item.component)}",
    ])
  }
  table(["component", "flow", "unit", "fraction"], rows)
}

///|
pub fn StreamInventory::to_markdown(self : StreamInventory) -> String {
  self.to_table().to_markdown()
}

///|
pub fn StreamInventory::to_json(self : StreamInventory) -> String {
  let values : Array[String] = []
  for item in self.items {
    values.push(
      "{\"component\":\{json_string(item.component)},\"flow\":\{item.flow},\"unit\":\{json_string(item.unit.symbol())}}",
    )
  }
  "{\"name\":{json_string(self.name)},\"items\":[" + values.join(",") + "]}"
}

///|
pub fn combine_by_component(
  streams : Array[StreamInventory],
  component : String,
) -> Float {
  let mut total : Float = 0.0
  for item in streams {
    total = total + item.component_flow(component)
  }
  total
}

///|
pub fn component_recovery(
  feed : StreamInventory,
  product : StreamInventory,
  component : String,
) -> Float {
  recovery(feed.component_flow(component), product.component_flow(component))
}

///|
pub fn component_loss(
  feed : StreamInventory,
  product : StreamInventory,
  component : String,
) -> Float {
  loss_fraction(
    feed.component_flow(component),
    product.component_flow(component),
  )
}

///|
pub fn stream_yield(feed : StreamInventory, product : StreamInventory) -> Float {
  recovery(feed.total_flow(), product.total_flow())
}

///|
pub fn stream_selectivity(
  product : StreamInventory,
  target : String,
  byproduct : String,
) -> Float {
  let denominator = product.component_flow(byproduct)
  if denominator == 0.0 {
    product.component_flow(target)
  } else {
    product.component_flow(target) / denominator
  }
}