// Cost basis inventories.  A position is a number of units of one currency
// augmented with an optional cost basis, mirroring Beancount's inventory model.

///|
pub(all) struct Position {
  units : @ast.Amount
  cost : @ast.Cost?
}

///|
pub fn Position::new(units : @ast.Amount, cost : @ast.Cost?) -> Position {
  { units, cost, }
}

///|
pub fn Position::to_string(self : Position) -> String {
  match self.cost {
    Some(cost) => self.units.to_string() + " " + cost.to_string()
    None => self.units.to_string()
  }
}

///|
fn cost_same(a : @ast.Cost?, b : @ast.Cost?) -> Bool {
  match (a, b) {
    (None, None) => true
    (Some(ca), Some(cb)) => {
      if !ca.number.equal(cb.number) || ca.currency != cb.currency {
        return false
      }
      let date_same = match (ca.date, cb.date) {
        (None, None) => true
        (Some(da), Some(db)) => da.equal(db)
        _ => false
      }
      if !date_same {
        return false
      }
      match (ca.label, cb.label) {
        (None, None) => true
        (Some(la), Some(lb)) => la == lb
        _ => false
      }
    }
    _ => false
  }
}

///|
pub(all) struct Inventory {
  positions : Array[Position]
}

///|
pub fn Inventory::new() -> Inventory {
  { positions: [], }
}

///|
pub fn Inventory::is_empty(self : Inventory) -> Bool {
  self.positions.length() == 0
}

///|
pub fn Inventory::quantity(
  self : Inventory,
  currency : String,
) -> @decimal.Decimal {
  let mut total = @decimal.Decimal::zero()
  for position in self.positions {
    if position.units.currency == currency {
      total = total.add(position.units.number)
    }
  }
  total
}

///|
pub fn Inventory::currencies(self : Inventory) -> Array[String] {
  let result = []
  for position in self.positions {
    if !result.contains(position.units.currency) {
      result.push(position.units.currency)
    }
  }
  result
}

///|
pub fn Inventory::add(self : Inventory, position : Position) -> Unit {
  for i in 0.. ignore
      } else {
        self.positions[i] = {
          units: @ast.Amount::new(total, existing.units.currency),
          cost: existing.cost,
        }
      }
      return
    }
  }
  if !position.units.number.is_zero() {
    self.positions.push(position)
  }
}

///|
pub fn Inventory::add_amount(self : Inventory, amount : @ast.Amount) -> Unit {
  self.add({ units: amount, cost: None, })
}

///|
pub fn Inventory::remove_at(self : Inventory, index : Int32) -> Unit {
  if index >= 0 && index < self.positions.length() {
    self.positions.remove(index) |> ignore
  }
}

///|
pub fn Inventory::copy(self : Inventory) -> Inventory {
  { positions: self.positions.copy(), }
}

///|
pub fn Inventory::to_string(self : Inventory) -> String {
  if self.positions.length() == 0 {
    return "(empty)"
  }
  let sb = StringBuilder()
  let mut first = true
  for position in self.positions {
    if !first {
      sb.write_string(", ")
    }
    sb.write_string(position.to_string())
    first = false
  }
  sb.to_string()
}

///|
pub fn Inventory::average_cost(
  self : Inventory,
  currency : String,
) -> @ast.Cost? {
  let mut units = @decimal.Decimal::zero()
  let mut total_cost = @decimal.Decimal::zero()
  let mut cost_currency : String? = None
  let mut label : String? = None
  for position in self.positions {
    if position.units.currency == currency {
      match position.cost {
        Some(cost) => {
          units = units.add(position.units.number)
          total_cost = total_cost.add(position.units.number.mul(cost.number))
          cost_currency = Some(cost.currency)
          if label is None {
            label = cost.label
          }
        }
        None => ()
      }
    }
  }
  if units.is_zero() {
    return None
  }
  match cost_currency {
    Some(currency) =>
      match total_cost.div(units) {
        Some(per) => Some(@ast.Cost::new(per, currency, None, label))
        None => None
      }
    None => None
  }
}

///|
/// Collapses every costed position in `currency` into a single lot held at the
/// weighted average cost.  Positions without a cost are left untouched.
pub fn Inventory::merge_average(self : Inventory, currency : String) -> Unit {
  let average = match self.average_cost(currency) {
    Some(cost) => cost
    None => return
  }
  let mut total = @decimal.Decimal::zero()
  let mut compatible = true
  for position in self.positions {
    if position.units.currency == currency {
      match position.cost {
        Some(cost) => {
          if cost.currency != average.currency {
            compatible = false
          }
          total = total.add(position.units.number)
        }
        None => ()
      }
    }
  }
  if !compatible || total.is_zero() {
    return
  }
  let mut i = self.positions.length() - 1
  while i >= 0 {
    let position = self.positions[i]
    if position.units.currency == currency && position.cost is Some(_) {
      self.positions.remove(i) |> ignore
    }
    i = i - 1
  }
  self.positions.push({
    units: @ast.Amount::new(total, currency),
    cost: Some(average),
  })
}

///|
pub(all) struct AccountInventory {
  inventory : Inventory
  booking : String?
}

///|
pub(all) struct BookingState {
  accounts : Map[String, AccountInventory]
}

///|
pub fn BookingState::new() -> BookingState {
  { accounts: {}, }
}

///|
pub fn BookingState::set_booking(
  self : BookingState,
  account : String,
  booking : String,
) -> Unit {
  match self.accounts.get(account) {
    Some(state) =>
      self.accounts.set(account, {
        inventory: state.inventory,
        booking: Some(booking),
      })
    None =>
      self.accounts.set(account, {
        inventory: Inventory::new(),
        booking: Some(booking),
      })
  }
}

///|
pub fn BookingState::inventory_for(
  self : BookingState,
  account : String,
) -> Inventory {
  match self.accounts.get(account) {
    Some(state) => state.inventory
    None => {
      let inventory = Inventory::new()
      self.accounts.set(account, { inventory, booking: None, })
      inventory
    }
  }
}

///|
pub fn BookingState::inventory(
  self : BookingState,
  account : String,
) -> Inventory? {
  match self.accounts.get(account) {
    Some(state) => Some(state.inventory)
    None => None
  }
}

///|
pub fn BookingState::method_for(
  self : BookingState,
  account : String,
  default_method : String,
) -> String {
  match self.accounts.get(account) {
    Some(state) =>
      match state.booking {
        Some(booking) => booking
        None => default_method
      }
    None => default_method
  }
}

///|
pub fn BookingState::accounts_list(self : BookingState) -> Array[String] {
  self.accounts.keys().to_array()
}