// Core data model for moonbean, kept independent from parsing and booking so
// that native, wasm-gc and js builds share the same representation.

///|
pub(all) struct Loc {
  file : String
  line : Int32
}

///|
pub fn Loc::new(file : String, line : Int32) -> Loc {
  { file, line, }
}

///|
pub fn Loc::to_string(self : Loc) -> String {
  self.file + ":" + self.line.to_string()
}

///|
pub(all) struct Diagnostic {
  code : String
  message : String
  loc : Loc
}

///|
pub fn Diagnostic::new(
  code : String,
  message : String,
  loc : Loc,
) -> Diagnostic {
  { code, message, loc, }
}

///|
pub fn Diagnostic::to_string(self : Diagnostic) -> String {
  self.loc.to_string() + ": " + self.message
}

///|
pub(all) enum MetaValue {
  MString(String)
  MNumber(@decimal.Decimal)
  MDate(Date)
  MAccount(String)
  MCurrency(String)
  MTag(String)
  MBool(Bool)
}

///|
pub fn MetaValue::to_string(self : MetaValue) -> String {
  match self {
    MString(s) => "\"" + s.replace_all(old="\"", new="\\\"") + "\""
    MNumber(n) => n.to_string()
    MDate(d) => d.to_string()
    MAccount(a) => a
    MCurrency(c) => c
    MTag(t) => "#" + t
    MBool(b) => if b { "TRUE" } else { "FALSE" }
  }
}

///|
pub(all) struct Meta {
  entries : Array[(String, MetaValue)]
}

///|
pub fn Meta::new() -> Meta {
  { entries: [], }
}

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

///|
pub fn Meta::get(self : Meta, key : String) -> MetaValue? {
  for entry in self.entries {
    if entry.0 == key {
      return Some(entry.1)
    }
  }
  None
}

///|
pub fn Meta::has(self : Meta, key : String) -> Bool {
  self.get(key) is Some(_)
}

///|
pub fn Meta::set(self : Meta, key : String, value : MetaValue) -> Unit {
  let mut replaced = false
  for i in 0.. Unit {
  let kept = []
  for entry in self.entries {
    if entry.0 != key {
      kept.push(entry)
    }
  }
  self.entries.clear()
  for entry in kept {
    self.entries.push(entry)
  }
}

///|
pub fn Meta::copy(self : Meta) -> Meta {
  { entries: self.entries.copy(), }
}

///|
pub(all) struct Amount {
  number : @decimal.Decimal
  currency : String
}

///|
pub fn Amount::new(number : @decimal.Decimal, currency : String) -> Amount {
  { number, currency, }
}

///|
pub fn Amount::to_string(self : Amount) -> String {
  self.number.to_string() + " " + self.currency
}

///|
pub(all) struct Cost {
  number : @decimal.Decimal
  currency : String
  date : Date?
  label : String?
}

///|
pub fn Cost::new(
  number : @decimal.Decimal,
  currency : String,
  date : Date?,
  label : String?,
) -> Cost {
  { number, currency, date, label, }
}

///|
pub fn Cost::to_string(self : Cost) -> String {
  let sb = StringBuilder()
  sb.write_string("{")
  sb.write_string(self.number.to_string())
  sb.write_string(" ")
  sb.write_string(self.currency)
  if self.date is Some(d) {
    sb.write_string(", ")
    sb.write_string(d.to_string())
  }
  if self.label is Some(l) {
    sb.write_string(", \"")
    sb.write_string(l)
    sb.write_string("\"")
  }
  sb.write_string("}")
  sb.to_string()
}

///|
pub(all) struct CostSpec {
  number_per : @decimal.Decimal?
  number_total : @decimal.Decimal?
  currency : String?
  date : Date?
  label : String?
  merge : Bool
  present : Bool
}

///|
pub fn CostSpec::empty() -> CostSpec {
  {
    number_per: None,
    number_total: None,
    currency: None,
    date: None,
    label: None,
    merge: false,
    present: true,
  }
}

///|
pub fn CostSpec::new_per(
  number : @decimal.Decimal,
  currency : String,
  date : Date?,
  label : String?,
) -> CostSpec {
  {
    number_per: Some(number),
    number_total: None,
    currency: Some(currency),
    date,
    label,
    merge: false,
    present: true,
  }
}

///|
pub fn CostSpec::new_total(
  number : @decimal.Decimal,
  currency : String,
) -> CostSpec {
  {
    number_per: None,
    number_total: Some(number),
    currency: Some(currency),
    date: None,
    label: None,
    merge: false,
    present: true,
  }
}

///|
pub fn CostSpec::is_empty_spec(self : CostSpec) -> Bool {
  self.number_per is None &&
  self.number_total is None &&
  self.currency is None &&
  self.date is None &&
  self.label is None &&
  !self.merge
}

///|
pub fn CostSpec::to_string(self : CostSpec) -> String {
  let sb = StringBuilder()
  if self.number_total is Some(total) {
    sb.write_string("{{")
    sb.write_string(total.to_string())
    if self.currency is Some(c) {
      sb.write_string(" ")
      sb.write_string(c)
    }
    sb.write_string("}}")
  } else {
    sb.write_string("{")
    let mut first = true
    if self.number_per is Some(number) {
      sb.write_string(number.to_string())
      if self.currency is Some(c) {
        sb.write_string(" ")
        sb.write_string(c)
      }
      first = false
    } else if self.currency is Some(c) {
      sb.write_string(c)
      first = false
    }
    if self.date is Some(d) {
      if !first {
        sb.write_string(", ")
      }
      sb.write_string(d.to_string())
      first = false
    }
    if self.label is Some(l) {
      if !first {
        sb.write_string(", ")
      }
      sb.write_string("\"")
      sb.write_string(l)
      sb.write_string("\"")
      first = false
    }
    if self.merge {
      if !first {
        sb.write_string(", ")
      }
      sb.write_string("*")
    }
    sb.write_string("}")
  }
  sb.to_string()
}

///|
pub(all) enum PriceAnno {
  PerUnit(Amount)
  Total(Amount)
}

///|
pub fn PriceAnno::amount(self : PriceAnno) -> Amount {
  match self {
    PerUnit(a) => a
    Total(a) => a
  }
}

///|
pub fn PriceAnno::to_string(self : PriceAnno) -> String {
  match self {
    PerUnit(a) => "@ " + a.to_string()
    Total(a) => "@@ " + a.to_string()
  }
}

///|
pub(all) struct Posting {
  mut flag : String?
  account : String
  mut units : Amount?
  mut units_currency : String?
  mut cost : CostSpec?
  mut price : PriceAnno?
  meta : Meta
  mut loc : Loc
}

///|
pub fn Posting::new(account : String) -> Posting {
  {
    flag: None,
    account,
    units: None,
    units_currency: None,
    cost: None,
    price: None,
    meta: Meta::new(),
    loc: Loc::new("", 0),
  }
}

///|
pub(all) struct Transaction {
  flag : String
  payee : String?
  narration : String
  tags : Array[String]
  links : Array[String]
  postings : Array[Posting]
}

///|
pub fn Transaction::new(
  flag : String,
  payee : String?,
  narration : String,
  tags : Array[String],
  links : Array[String],
) -> Transaction {
  { flag, payee, narration, tags, links, postings: [], }
}

///|
pub(all) struct Open {
  account : String
  currencies : Array[String]
  booking : String?
}

///|
pub(all) struct Close {
  account : String
}

///|
pub(all) struct Commodity {
  currency : String
}

///|
pub(all) struct Pad {
  account : String
  source_account : String
}

///|
pub(all) struct Balance {
  account : String
  amount : Amount
  tolerance : @decimal.Decimal?
}

///|
pub(all) struct Price {
  currency : String
  amount : Amount
}

///|
pub(all) struct Event {
  event_type : String
  description : String
}

///|
pub(all) struct QueryDirective {
  name : String
  query : String
}

///|
pub(all) struct Note {
  account : String
  comment : String
}

///|
pub(all) struct Document {
  account : String
  filename : String
  tags : Array[String]
  links : Array[String]
}

///|
pub(all) enum CustomValue {
  CString(String)
  CBool(Bool)
  CDate(Date)
  CNumber(@decimal.Decimal)
  CAmount(Amount)
  CAccount(String)
  CCurrency(String)
  CTag(String)
}

///|
pub fn CustomValue::to_string(self : CustomValue) -> String {
  match self {
    CString(s) => "\"" + s + "\""
    CBool(b) => if b { "TRUE" } else { "FALSE" }
    CDate(d) => d.to_string()
    CNumber(n) => n.to_string()
    CAmount(a) => a.to_string()
    CAccount(a) => a
    CCurrency(c) => c
    CTag(t) => "#" + t
  }
}

///|
pub(all) struct Custom {
  custom_type : String
  values : Array[CustomValue]
}

///|
pub(all) enum Directive {
  DOpen(Open)
  DClose(Close)
  DCommodity(Commodity)
  DPad(Pad)
  DBalance(Balance)
  DPrice(Price)
  DEvent(Event)
  DQuery(QueryDirective)
  DCustom(Custom)
  DNote(Note)
  DDocument(Document)
  DTransaction(Transaction)
}

///|
pub(all) struct Entry {
  date : Date
  loc : Loc
  mut meta : Meta
  directive : Directive
}

///|
pub fn Entry::new(date : Date, loc : Loc, directive : Directive) -> Entry {
  { date, loc, meta: Meta::new(), directive, }
}

///|
pub fn Entry::is_transaction(self : Entry) -> Bool {
  self.directive is DTransaction(_)
}

///|
pub(all) struct Plugin {
  name : String
  config : String?
  loc : Loc
}

///|
pub(all) struct Options {
  mut title : String?
  mut booking_method : String
  mut tolerance_multiplier : @decimal.Decimal
  inferred_tolerance_default : Map[String, @decimal.Decimal]
  mut infer_tolerance_from_cost : Bool
  mut account_rounding : String?
  operating_currencies : Array[String]
  documents : Array[String]
  display_precision : Map[String, @decimal.Decimal]
  mut render_commas : Bool
  raw : Array[(String, String)]
}

///|
pub fn Options::new() -> Options {
  {
    title: None,
    booking_method: "STRICT",
    tolerance_multiplier: @decimal.Decimal::parse("0.5").unwrap(),
    inferred_tolerance_default: {},
    infer_tolerance_from_cost: false,
    account_rounding: None,
    operating_currencies: [],
    documents: [],
    display_precision: {},
    render_commas: false,
    raw: [],
  }
}

///|
pub fn Options::add_raw(self : Options, name : String, value : String) -> Unit {
  self.raw.push((name, value))
}

///|
pub fn Options::get(self : Options, name : String) -> String? {
  let mut result = None
  for pair in self.raw {
    if pair.0 == name {
      result = Some(pair.1)
    }
  }
  result
}

///|
pub fn Options::get_all(self : Options, name : String) -> Array[String] {
  let result = []
  for pair in self.raw {
    if pair.0 == name {
      result.push(pair.1)
    }
  }
  result
}

///|
pub(all) struct Ledger {
  entries : Array[Entry]
  errors : Array[Diagnostic]
  mut options : Options
  plugins : Array[Plugin]
  includes : Array[(String, Loc)]
}

///|
pub fn Ledger::new() -> Ledger {
  {
    entries: [],
    errors: [],
    options: Options::new(),
    plugins: [],
    includes: [],
  }
}

///|
pub fn Ledger::add_error(self : Ledger, error : Diagnostic) -> Unit {
  self.errors.push(error)
}

///|
pub fn account_parts(account : String) -> Array[String] {
  let parts = []
  for part in account.split(":") {
    parts.push(String::from_iter(part.iter()))
  }
  parts
}

///|
pub fn account_root(account : String) -> String {
  let parts = account_parts(account)
  if parts.length() == 0 {
    ""
  } else {
    parts[0]
  }
}

///|
pub fn account_leaf(account : String) -> String {
  let parts = account_parts(account)
  if parts.length() == 0 {
    ""
  } else {
    parts[parts.length() - 1]
  }
}

///|
pub fn account_parent(account : String) -> String {
  let parts = account_parts(account)
  if parts.length() <= 1 {
    ""
  } else {
    let sb = StringBuilder()
    let mut i = 0
    while i < parts.length() - 1 {
      if i > 0 {
        sb.write_string(":")
      }
      sb.write_string(parts[i])
      i = i + 1
    }
    sb.to_string()
  }
}

///|
/// Resolves an equity subaccount option, including the configured equity root.
pub fn Options::equity_account(
  self : Options,
  option : String,
  default_suffix : String,
) -> String {
  let mut root = "Equity"
  let mut suffix = default_suffix
  for (key, value) in self.raw {
    if key == "name_equity" {
      root = value
    }
    if key == option {
      suffix = value
    }
  }
  root + ":" + suffix
}