///|
fn exports_range_json(range : ExportByteRange) -> Json {
  let row : Map[String, Json] = Map([])
  row.set("offset", int_json(range.offset))
  row.set("size", int_json(range.size))
  Json::object(row)
}

///|
fn exports_optional_range_json(range : ExportByteRange?) -> Json {
  match range {
    Some(range) => exports_range_json(range)
    None => Json::null()
  }
}

///|
fn exports_ledger_json(l : ExportsLedger) -> Json {
  let ledger : Map[String, Json] = Map([])
  for
    (key, value) in [
      ("export_section_bytes", l.export_section_bytes),
      ("section_header_bytes", l.section_header_bytes),
      ("count_prefix_bytes", l.count_prefix_bytes),
      ("declaration_bytes", l.declaration_bytes),
      ("name_encoding_bytes", l.name_encoding_bytes),
      ("name_length_prefix_bytes", l.name_length_prefix_bytes),
      ("name_utf8_bytes", l.name_utf8_bytes),
      ("descriptor_bytes", l.descriptor_bytes),
      ("kind_bytes", l.kind_bytes),
      ("target_index_prefix_bytes", l.target_index_prefix_bytes),
    ] {
    ledger.set(key, int_json(value))
  }
  Json::object(ledger)
}

///|
fn exports_declaration_json(entry : ExportDeclaration) -> Json {
  let row : Map[String, Json] = Map([])
  row.set("ordinal", int_json(entry.ordinal))
  row.set("name", Json::string(entry.name))
  row.set("kind", Json::string(entry.kind))
  row.set("kind_code", int_json(entry.kind_code))
  row.set("target_index", metadata_uint_json(entry.target_index))
  row.set("encoding", exports_range_json(entry.encoding))
  row.set("name_encoding", exports_range_json(entry.name_encoding))
  row.set("name_length_prefix_bytes", int_json(entry.name_length_prefix_bytes))
  row.set("name_utf8_bytes", int_json(entry.name_utf8_bytes))
  row.set("descriptor", exports_range_json(entry.descriptor))
  row.set(
    "target_index_prefix_bytes",
    int_json(entry.target_index_prefix_bytes),
  )
  Json::object(row)
}

///|
fn exports_json_value(report : ExportsReport) -> Json {
  let root : Map[String, Json] = Map([])
  root.set("format", Json::string("moonsize-exports"))
  root.set("schema_version", int_json(1))
  root.set("file_size", int_json(report.file_size))
  root.set("sha256", Json::string(report.sha256))
  root.set("section", exports_optional_range_json(report.section))
  root.set("count_encoding", exports_optional_range_json(report.count_encoding))
  let summary : Map[String, Json] = Map([])
  let s = report.summary
  summary.set("export_count", int_json(s.export_count))
  summary.set("distinct_target_count", int_json(s.distinct_target_count))
  summary.set("alias_group_count", int_json(s.alias_group_count))
  summary.set("alias_member_count", int_json(s.alias_member_count))
  summary.set("singleton_target_count", int_json(s.singleton_target_count))
  let counts : Map[String, Json] = Map([])
  for
    (kind, count) in [
      ("function", s.kind_counts.functions),
      ("table", s.kind_counts.tables),
      ("memory", s.kind_counts.memories),
      ("global", s.kind_counts.globals),
      ("tag", s.kind_counts.tags),
    ] {
    counts.set(kind, int_json(count))
  }
  summary.set("kind_counts", Json::object(counts))
  root.set("summary", Json::object(summary))
  root.set("ledger", exports_ledger_json(report.ledger))
  root.set("entries", Json::array(report.entries.map(exports_declaration_json)))
  root.set(
    "targets",
    Json::array(
      report.targets.map(fn(target) {
        let row : Map[String, Json] = Map([])
        row.set("kind", Json::string(target.kind))
        row.set("kind_code", int_json(target.kind_code))
        row.set("target_index", metadata_uint_json(target.target_index))
        row.set(
          "entry_ordinals",
          Json::array(target.entry_ordinals.map(int_json)),
        )
        row.set("member_count", int_json(target.member_count))
        row.set("declaration_bytes", int_json(target.declaration_bytes))
        row.set("is_alias", Json::boolean(target.is_alias))
        Json::object(row)
      }),
    ),
  )
  root.set("supported_scope", Json::string(report.supported_scope))
  Json::object(root)
}

///|
pub fn render_exports_json(report : ExportsReport) -> String {
  exports_json_value(report).stringify(indent=2) + "\n"
}

///|
pub fn render_exports(report : ExportsReport, top : Int) -> String {
  let out = StringBuilder()
  let s = report.summary
  let c = s.kind_counts
  let l = report.ledger
  out.write_string(
    "EXPORT DECLARATIONS\n\nInput: \{report.file_size} B; SHA-256 \{report.sha256}\n",
  )
  out.write_string(
    "Kinds: function \{c.functions}; table \{c.tables}; memory \{c.memories}; global \{c.globals}; tag \{c.tags}\n",
  )
  out.write_string(
    "Declarations: \{s.export_count}; distinct declared targets: \{s.distinct_target_count}\nAlias groups: \{s.alias_group_count}; alias members: \{s.alias_member_count}; singleton targets: \{s.singleton_target_count}\n",
  )
  if report.section is None {
    out.write_string("No export section.\n")
  } else if s.export_count == 0 {
    out.write_string("Export section has zero declarations.\n")
  }
  out.write_string(
    "\nENCODING LEDGER\nExport section: \{l.export_section_bytes} B = headers \{l.section_header_bytes} B + count \{l.count_prefix_bytes} B + declarations \{l.declaration_bytes} B\n",
  )
  out.write_string(
    "Declarations: \{l.declaration_bytes} B = name encodings \{l.name_encoding_bytes} B + descriptors \{l.descriptor_bytes} B\nName encodings: \{l.name_encoding_bytes} B = length prefixes \{l.name_length_prefix_bytes} B + UTF-8 \{l.name_utf8_bytes} B\nDescriptors: \{l.descriptor_bytes} B = kind \{l.kind_bytes} B + index prefixes \{l.target_index_prefix_bytes} B\n",
  )
  let entries = report.entries.copy()
  entries.sort_by(fn(a, b) {
    let order = b.encoding.size.compare(a.encoding.size)
    if order != 0 {
      order
    } else {
      a.ordinal.compare(b.ordinal)
    }
  })
  let shown = top.max(0).min(entries.length())
  out.write_string(
    "\nDECLARATIONS: showing \{shown} of \{entries.length()} (encoded size descending)\nORDINAL  KIND  TARGET INDEX  ENCODED B  FILE OFFSET  NAME\n",
  )
  for i in 0..