// Realization: replay validated transactions into inventories up to a chosen
// date.  Validation has already resolved missing amounts and inferred costs,
// so a replay is deterministic and gives reports historical cost lots.

///|
pub(all) struct RealizedLedger {
  entries : Array[@ast.Entry]
  at : @ast.Date?
  options : @ast.Options
  inventories : Map[String, @booking.Inventory]
  accounts : Map[String, @validation.AccountInfo]
  prices : PriceDb
}

///|
fn entry_in_range(entry : @ast.Entry, at : @ast.Date?) -> Bool {
  match at {
    Some(date) => entry.date.less_equal(date)
    None => true
  }
}

///|
fn replay_inventories(
  ledger : @validation.ValidatedLedger,
  at : @ast.Date?,
) -> Map[String, @booking.Inventory] {
  let inventories : Map[String, @booking.Inventory] = Map([])
  for name in ledger.accounts.keys().to_array() {
    inventories.set(name, @booking.Inventory::new())
  }
  for entry in ledger.entries {
    if entry_in_range(entry, at) {
      if entry.directive is DTransaction(txn) {
        for posting in txn.postings {
          if posting.units is Some(units) {
            let inventory = match inventories.get(posting.account) {
              Some(inv) => inv
              None => {
                let inv = @booking.Inventory::new()
                inventories.set(posting.account, inv)
                inv
              }
            }
            let cost = match posting.cost {
              Some(spec) =>
                match (spec.number_per, spec.currency) {
                  (Some(number), Some(currency)) =>
                    Some(
                      @ast.Cost::new(number, currency, spec.date, spec.label),
                    )
                  _ => None
                }
              None => None
            }
            if posting.cost.map(fn(c) { c.merge }).unwrap_or(false) {
              inventory.merge_average(units.currency)
            }
            inventory.add(@booking.Position::new(units, cost))
            if ledger.options.booking_method == "AVERAGE" ||
              ledger.accounts.get(posting.account).map(fn(a) { a.booking }) ==
              Some(Some("AVERAGE")) {
              inventory.merge_average(units.currency)
            }
          }
        }
      }
    }
  }
  inventories
}

///|
/// Builds the realized view of a validated ledger, optionally stopping at a
/// date (balance assertions and other non-transaction directives are kept for
/// querying, but only transactions affect inventories).
pub fn realize(
  ledger : @validation.ValidatedLedger,
  at~ : @ast.Date? = None,
) -> RealizedLedger {
  let entries = []
  for entry in ledger.entries {
    if entry_in_range(entry, at) {
      entries.push(entry)
    }
  }
  {
    entries,
    at,
    options: ledger.options,
    inventories: replay_inventories(ledger, at),
    accounts: ledger.accounts,
    prices: PriceDb::from_entries(entries),
  }
}

///|
pub fn RealizedLedger::inventory(
  self : RealizedLedger,
  account : String,
) -> @booking.Inventory {
  match self.inventories.get(account) {
    Some(inventory) => inventory.copy()
    None => @booking.Inventory::new()
  }
}

///|
pub fn RealizedLedger::account_names(self : RealizedLedger) -> Array[String] {
  let names = self.inventories.keys().to_array()
  let mut i = 1
  while i < names.length() {
    let current = names[i]
    let mut j = i
    while j > 0 {
      let prev = names[j - 1]
      if prev.lexical_compare(current) > 0 {
        names[j - 1] = current
        names[j] = prev
        j = j - 1
      } else {
        break
      }
    }
    i = i + 1
  }
  names
}

///|
pub fn is_subaccount(name : String, parent : String) -> Bool {
  if name == parent {
    return true
  }
  let name_parts = @ast.account_parts(name)
  let parent_parts = @ast.account_parts(parent)
  if name_parts.length() <= parent_parts.length() {
    return false
  }
  for i in 0.. @booking.Inventory {
  let result = @booking.Inventory::new()
  for name in self.account_names() {
    if name == account || (subtree && is_subaccount(name, account)) {
      match self.inventories.get(name) {
        Some(inventory) =>
          for position in inventory.positions {
            result.add(position)
          }
        None => ()
      }
    }
  }
  result
}

///|
pub fn RealizedLedger::rate(
  self : RealizedLedger,
  base : String,
  quote : String,
) -> @decimal.Decimal? {
  self.prices.rate(base, quote, self.at)
}

///|
pub fn RealizedLedger::convert(
  self : RealizedLedger,
  amount : @ast.Amount,
  target : String,
) -> @ast.Amount? {
  self.prices.convert(amount, target, self.at)
}