// JSON export of a validated ledger.  The shape is intentionally stable and
// flat so that other tools can diff it against bean-query output.

///|
fn jstr(text : String) -> String {
  "\"" + @realization.json_escape(text) + "\""
}

///|
fn jopt(text : String?) -> String {
  match text {
    Some(value) => jstr(value)
    None => "null"
  }
}

///|
fn jnum(number : @decimal.Decimal) -> String {
  jstr(number.to_string())
}

///|
fn jdate(date : @ast.Date) -> String {
  jstr(date.to_string())
}

///|
fn jamount(amount : @ast.Amount) -> String {
  "{\"number\":" +
  jnum(amount.number) +
  ",\"currency\":" +
  jstr(amount.currency) +
  "}"
}

///|
fn jmeta(meta : @ast.Meta) -> String {
  let sb = StringBuilder()
  sb.write_string("{")
  for i in 0.. 0 {
      sb.write_string(",")
    }
    let (key, value) = meta.entries[i]
    sb.write_string(jstr(key))
    sb.write_string(":")
    sb.write_string(
      match value {
        @ast.MString(text) => jstr(text)
        @ast.MNumber(number) => jnum(number)
        @ast.MDate(date) => jdate(date)
        @ast.MAccount(account) => jstr(account)
        @ast.MCurrency(currency) => jstr(currency)
        @ast.MTag(tag) => jstr(tag)
        @ast.MBool(flag) => if flag { "true" } else { "false" }
      },
    )
  }
  sb.write_string("}")
  sb.to_string()
}

///|
fn jloc(loc : @ast.Loc) -> String {
  "{\"file\":" + jstr(loc.file) + ",\"line\":" + loc.line.to_string() + "}"
}

///|
fn jstring_array(values : Array[String]) -> String {
  let sb = StringBuilder()
  sb.write_string("[")
  for i in 0.. 0 {
      sb.write_string(",")
    }
    sb.write_string(jstr(values[i]))
  }
  sb.write_string("]")
  sb.to_string()
}

///|
fn jcost(spec : @ast.CostSpec) -> String {
  let sb = StringBuilder()
  sb.write_string("{")
  sb.write_string("\"per\":")
  sb.write_string(
    match spec.number_per {
      Some(number) => jnum(number)
      None => "null"
    },
  )
  sb.write_string(",\"total\":")
  sb.write_string(
    match spec.number_total {
      Some(number) => jnum(number)
      None => "null"
    },
  )
  sb.write_string(",\"currency\":")
  sb.write_string(jopt(spec.currency))
  sb.write_string(",\"date\":")
  sb.write_string(
    match spec.date {
      Some(date) => jdate(date)
      None => "null"
    },
  )
  sb.write_string(",\"label\":")
  sb.write_string(jopt(spec.label))
  sb.write_string(",\"merge\":")
  sb.write_string(if spec.merge { "true" } else { "false" })
  sb.write_string("}")
  sb.to_string()
}

///|
fn jprice(price : @ast.PriceAnno) -> String {
  let amount = price.amount()
  let total = match price {
    @ast.PerUnit(_) => false
    @ast.Total(_) => true
  }
  "{\"number\":" +
  jnum(amount.number) +
  ",\"currency\":" +
  jstr(amount.currency) +
  ",\"total\":" +
  (if total { "true" } else { "false" }) +
  "}"
}

///|
fn jposting(posting : @ast.Posting) -> String {
  let sb = StringBuilder()
  sb.write_string("{")
  sb.write_string("\"flag\":")
  sb.write_string(jopt(posting.flag))
  sb.write_string(",\"account\":")
  sb.write_string(jstr(posting.account))
  sb.write_string(",\"units\":")
  sb.write_string(
    match posting.units {
      Some(units) => jamount(units)
      None => "null"
    },
  )
  sb.write_string(",\"cost\":")
  sb.write_string(
    match posting.cost {
      Some(spec) => jcost(spec)
      None => "null"
    },
  )
  sb.write_string(",\"price\":")
  sb.write_string(
    match posting.price {
      Some(price) => jprice(price)
      None => "null"
    },
  )
  sb.write_string(",\"meta\":")
  sb.write_string(jmeta(posting.meta))
  sb.write_string("}")
  sb.to_string()
}

///|
fn jpostings(postings : Array[@ast.Posting]) -> String {
  let sb = StringBuilder()
  sb.write_string("[")
  for i in 0.. 0 {
      sb.write_string(",")
    }
    sb.write_string(jposting(postings[i]))
  }
  sb.write_string("]")
  sb.to_string()
}

///|
fn jcustom_value(value : @ast.CustomValue) -> String {
  match value {
    @ast.CString(text) => "{\"kind\":\"string\",\"value\":" + jstr(text) + "}"
    @ast.CBool(flag) =>
      "{\"kind\":\"bool\",\"value\":" +
      (if flag { "true" } else { "false" }) +
      "}"
    @ast.CDate(date) => "{\"kind\":\"date\",\"value\":" + jdate(date) + "}"
    @ast.CNumber(number) =>
      "{\"kind\":\"number\",\"value\":" + jnum(number) + "}"
    @ast.CAmount(amount) =>
      "{\"kind\":\"amount\",\"value\":" + jamount(amount) + "}"
    @ast.CAccount(account) =>
      "{\"kind\":\"account\",\"value\":" + jstr(account) + "}"
    @ast.CCurrency(currency) =>
      "{\"kind\":\"currency\",\"value\":" + jstr(currency) + "}"
    @ast.CTag(tag) => "{\"kind\":\"tag\",\"value\":" + jstr(tag) + "}"
  }
}

///|
fn jcustom(custom : @ast.Custom) -> String {
  let sb = StringBuilder()
  sb.write_string("{\"type\":\"custom\",\"custom_type\":")
  sb.write_string(jstr(custom.custom_type))
  sb.write_string(",\"values\":[")
  for i in 0.. 0 {
      sb.write_string(",")
    }
    sb.write_string(jcustom_value(custom.values[i]))
  }
  sb.write_string("]}")
  sb.to_string()
}

///|
fn jdirective(entry : @ast.Entry) -> String {
  match entry.directive {
    @ast.DOpen(open) =>
      "{\"type\":\"open\",\"account\":" +
      jstr(open.account) +
      ",\"currencies\":" +
      jstring_array(open.currencies) +
      ",\"booking\":" +
      jopt(open.booking) +
      "}"
    @ast.DClose(close) =>
      "{\"type\":\"close\",\"account\":" + jstr(close.account) + "}"
    @ast.DCommodity(commodity) =>
      "{\"type\":\"commodity\",\"currency\":" + jstr(commodity.currency) + "}"
    @ast.DPad(pad) =>
      "{\"type\":\"pad\",\"account\":" +
      jstr(pad.account) +
      ",\"source\":" +
      jstr(pad.source_account) +
      "}"
    @ast.DBalance(balance) =>
      "{\"type\":\"balance\",\"account\":" +
      jstr(balance.account) +
      ",\"amount\":" +
      jamount(balance.amount) +
      ",\"tolerance\":" +
      (match balance.tolerance {
        Some(tolerance) => jnum(tolerance)
        None => "null"
      }) +
      "}"
    @ast.DPrice(price) =>
      "{\"type\":\"price\",\"currency\":" +
      jstr(price.currency) +
      ",\"amount\":" +
      jamount(price.amount) +
      "}"
    @ast.DEvent(event) =>
      "{\"type\":\"event\",\"event_type\":" +
      jstr(event.event_type) +
      ",\"description\":" +
      jstr(event.description) +
      "}"
    @ast.DQuery(query) =>
      "{\"type\":\"query\",\"name\":" +
      jstr(query.name) +
      ",\"query\":" +
      jstr(query.query) +
      "}"
    @ast.DCustom(custom) => jcustom(custom)
    @ast.DNote(note) =>
      "{\"type\":\"note\",\"account\":" +
      jstr(note.account) +
      ",\"comment\":" +
      jstr(note.comment) +
      "}"
    @ast.DDocument(document) =>
      "{\"type\":\"document\",\"account\":" +
      jstr(document.account) +
      ",\"filename\":" +
      jstr(document.filename) +
      ",\"tags\":" +
      jstring_array(document.tags) +
      ",\"links\":" +
      jstring_array(document.links) +
      "}"
    @ast.DTransaction(txn) =>
      "{\"type\":\"transaction\",\"flag\":" +
      jstr(txn.flag) +
      ",\"payee\":" +
      jopt(txn.payee) +
      ",\"narration\":" +
      jstr(txn.narration) +
      ",\"tags\":" +
      jstring_array(txn.tags) +
      ",\"links\":" +
      jstring_array(txn.links) +
      ",\"postings\":" +
      jpostings(txn.postings) +
      "}"
  }
}

///|
fn jentry(entry : @ast.Entry) -> String {
  let directive = jdirective(entry)
  let prefix = "{\"date\":" +
    jdate(entry.date) +
    ",\"loc\":" +
    jloc(entry.loc) +
    ",\"meta\":" +
    jmeta(entry.meta) +
    ","
  prefix + lib_view(directive[1:])
}

///|
fn jdiagnostic(diagnostic : @ast.Diagnostic) -> String {
  "{\"file\":" +
  jstr(diagnostic.loc.file) +
  ",\"line\":" +
  diagnostic.loc.line.to_string() +
  ",\"code\":" +
  jstr(diagnostic.code) +
  ",\"message\":" +
  jstr(diagnostic.message) +
  "}"
}

///|
fn jdiagnostics(diagnostics : Array[@ast.Diagnostic]) -> String {
  let sb = StringBuilder()
  sb.write_string("[")
  for i in 0.. 0 {
      sb.write_string(",")
    }
    sb.write_string(jdiagnostic(diagnostics[i]))
  }
  sb.write_string("]")
  sb.to_string()
}

///|
fn jaccount(info : @validation.AccountInfo) -> String {
  "{\"account\":" +
  jstr(info.account) +
  ",\"open\":" +
  jdate(info.open_date) +
  ",\"close\":" +
  (match info.close_date {
    Some(date) => jdate(date)
    None => "null"
  }) +
  ",\"currencies\":" +
  jstring_array(info.currencies) +
  ",\"booking\":" +
  jopt(info.booking) +
  ",\"loc\":" +
  jloc(info.loc) +
  "}"
}

///|
fn jaccounts(ledger : @validation.ValidatedLedger) -> String {
  let names = ledger.account_names()
  let sb = StringBuilder()
  sb.write_string("[")
  for i in 0.. 0 {
      sb.write_string(",")
    }
    match ledger.account_info(names[i]) {
      Some(info) => sb.write_string(jaccount(info))
      None => ()
    }
  }
  sb.write_string("]")
  sb.to_string()
}

///|
/// Serializes a validated ledger to JSON.  Amounts are emitted as strings so
/// that exact decimal values survive the round trip.
pub fn export_json(
  file : String,
  ledger : @validation.ValidatedLedger,
) -> String {
  let sb = StringBuilder()
  sb.write_string("{\"file\":")
  sb.write_string(jstr(file))
  sb.write_string(",\"errors\":")
  sb.write_string(jdiagnostics(ledger.errors))
  sb.write_string(",\"warnings\":")
  sb.write_string(jdiagnostics(ledger.warnings))
  sb.write_string(",\"accounts\":")
  sb.write_string(jaccounts(ledger))
  sb.write_string(",\"entries\":[")
  for i in 0.. 0 {
      sb.write_string(",")
    }
    sb.write_string(jentry(ledger.entries[i]))
  }
  sb.write_string("]}")
  sb.to_string()
}