///|
fn metadata_range_json(range : MetadataRange) -> 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 metadata_optional_range_json(range : MetadataRange?) -> Json {
  match range {
    Some(r) => metadata_range_json(r)
    None => Json::null()
  }
}

///|
/// All u32 values are exact in Double (53 significant bits); repr preserves the
/// unsigned decimal spelling, including values above the signed Int maximum.
fn metadata_uint_json(value : UInt) -> Json {
  Json::number(value.to_double(), repr=value.to_string())
}

///|
fn metadata_json_value(report : MetadataReport) -> Json {
  let root : Map[String, Json] = Map([])
  root.set("format", Json::string("moonsize-metadata"))
  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(
    "custom_fraction",
    Json::number(
      report.summary.custom_bytes.to_double() / report.file_size.to_double(),
    ),
  )
  root.set("byte_ledger_complete", Json::boolean(report.byte_ledger_complete))
  root.set("custom_name_status", Json::string(report.custom_name_status))
  root.set("name_details_status", Json::string(report.name_details_status))
  root.set("modeling_scope", Json::string(report.modeling_scope))
  let s = report.summary
  let summary : Map[String, Json] = Map([])
  summary.set("custom_count", int_json(s.custom_count))
  summary.set("custom_bytes", int_json(s.custom_bytes))
  summary.set("section_header_bytes", int_json(s.section_header_bytes))
  summary.set(
    "custom_name_encoding_bytes",
    int_json(s.custom_name_encoding_bytes),
  )
  summary.set("content_bytes", int_json(s.content_bytes))
  summary.set("unavailable_custom_count", int_json(s.unavailable_custom_count))
  summary.set("unavailable_custom_bytes", int_json(s.unavailable_custom_bytes))
  summary.set("name_custom_count", int_json(s.name_custom_count))
  summary.set("name_content_bytes", int_json(s.name_content_bytes))
  summary.set("name_subsection_bytes", int_json(s.name_subsection_bytes))
  summary.set(
    "unbounded_name_tail_bytes",
    int_json(s.unbounded_name_tail_bytes),
  )
  summary.set(
    "function_name_payload_bytes",
    int_json(s.function_name_payload_bytes),
  )
  summary.set(
    "function_name_count_prefix_bytes",
    int_json(s.function_name_count_prefix_bytes),
  )
  summary.set(
    "parsed_function_record_count",
    int_json(s.parsed_function_record_count),
  )
  summary.set(
    "parsed_function_record_bytes",
    int_json(s.parsed_function_record_bytes),
  )
  summary.set(
    "function_name_unparsed_bytes",
    int_json(s.function_name_unparsed_bytes),
  )
  summary.set(
    "function_index_prefix_bytes",
    int_json(s.function_index_prefix_bytes),
  )
  summary.set(
    "function_name_length_prefix_bytes",
    int_json(s.function_name_length_prefix_bytes),
  )
  summary.set("function_name_utf8_bytes", int_json(s.function_name_utf8_bytes))
  summary.set(
    "unavailable_function_name_count",
    int_json(s.unavailable_function_name_count),
  )
  root.set("summary", Json::object(summary))
  root.set(
    "categories",
    Json::array(
      report.categories.map(fn(group) {
        let row : Map[String, Json] = Map([])
        row.set("category", Json::string(group.category))
        row.set(
          "custom_ordinals",
          Json::array(group.custom_ordinals.map(int_json)),
        )
        row.set("total_bytes", int_json(group.total_bytes))
        Json::object(row)
      }),
    ),
  )
  root.set(
    "name_groups",
    Json::array(
      report.name_groups.map(fn(group) {
        let row : Map[String, Json] = Map([])
        row.set("name", path_optional_string(group.name))
        row.set(
          "custom_ordinals",
          Json::array(group.custom_ordinals.map(int_json)),
        )
        row.set("total_bytes", int_json(group.total_bytes))
        Json::object(row)
      }),
    ),
  )
  root.set(
    "customs",
    Json::array(
      report.customs.map(fn(item) {
        let row : Map[String, Json] = Map([])
        row.set("ordinal", int_json(item.ordinal))
        row.set("section_ordinal", int_json(item.section_ordinal))
        row.set("encoding", metadata_range_json(item.encoding))
        row.set("section_header_bytes", int_json(item.section_header_bytes))
        row.set("name_encoding", metadata_range_json(item.name_encoding))
        row.set(
          "name_length_prefix_bytes",
          int_json(item.name_length_prefix_bytes),
        )
        row.set("name_bytes", metadata_range_json(item.name_bytes))
        row.set("name", path_optional_string(item.name))
        row.set("category", Json::string(item.category))
        row.set("content", metadata_range_json(item.content))
        row.set("status", Json::string(item.status))
        row.set("name_subsection_bytes", int_json(item.name_subsection_bytes))
        row.set(
          "unbounded_tail",
          metadata_optional_range_json(item.unbounded_tail),
        )
        Json::object(row)
      }),
    ),
  )
  root.set(
    "subsections",
    Json::array(
      report.subsections.map(fn(sub) {
        let row : Map[String, Json] = Map([])
        row.set("custom_ordinal", int_json(sub.custom_ordinal))
        row.set("ordinal", int_json(sub.ordinal))
        row.set("id", int_json(sub.id))
        row.set("encoding", metadata_range_json(sub.encoding))
        row.set("header_bytes", int_json(sub.header_bytes))
        row.set("payload", metadata_range_json(sub.payload))
        row.set("status", Json::string(sub.status))
        row.set(
          "declared_count",
          match sub.declared_count {
            Some(n) => metadata_uint_json(n)
            None => Json::null()
          },
        )
        row.set("count_prefix_bytes", int_json(sub.count_prefix_bytes))
        row.set("parsed_record_bytes", int_json(sub.parsed_record_bytes))
        row.set("unparsed", metadata_optional_range_json(sub.unparsed))
        Json::object(row)
      }),
    ),
  )
  root.set(
    "function_names",
    Json::array(
      report.function_names.map(fn(record) {
        let row : Map[String, Json] = Map([])
        row.set("custom_ordinal", int_json(record.custom_ordinal))
        row.set("subsection_ordinal", int_json(record.subsection_ordinal))
        row.set("ordinal", int_json(record.ordinal))
        row.set("function_index", metadata_uint_json(record.function_index))
        row.set("encoding", metadata_range_json(record.encoding))
        row.set("index_prefix_bytes", int_json(record.index_prefix_bytes))
        row.set(
          "name_length_prefix_bytes",
          int_json(record.name_length_prefix_bytes),
        )
        row.set("name_bytes", metadata_range_json(record.name_bytes))
        row.set("name", path_optional_string(record.name))
        Json::object(row)
      }),
    ),
  )
  root.set(
    "diagnostics",
    Json::array(
      report.diagnostics.map(fn(diagnostic) {
        let row : Map[String, Json] = Map([])
        row.set("offset", int_json(diagnostic.offset))
        row.set("custom_ordinal", int_json(diagnostic.custom_ordinal))
        row.set(
          "subsection_ordinal",
          optional_index_json(diagnostic.subsection_ordinal),
        )
        row.set("reason", Json::string(diagnostic.reason))
        Json::object(row)
      }),
    ),
  )
  Json::object(root)
}

///|
pub fn render_metadata_json(report : MetadataReport) -> String {
  metadata_json_value(report).stringify(indent=2) + "\n"
}

///|
fn metadata_range_text(range : MetadataRange) -> String {
  "file offset \{range.offset}, \{range.size} B"
}

///|
pub fn render_metadata(report : MetadataReport, top : Int) -> String {
  let out = StringBuilder()
  let s = report.summary
  out.write_string("METADATA FILE BYTES\n\n")
  out.write_string("Input: \{report.file_size} B; SHA-256 \{report.sha256}\n")
  out.write_string(
    "Custom sections: \{s.custom_count}; \{s.custom_bytes} B (" +
    share_text(s.custom_bytes, report.file_size) +
    " of file)\n",
  )
  out.write_string(
    "Custom bytes: \{s.custom_bytes} B = section headers \{s.section_header_bytes} B + custom-name encodings \{s.custom_name_encoding_bytes} B + contents \{s.content_bytes} B\n",
  )
  out.write_string("\nCATEGORY SUMMARY (all instances)\n")
  for group in report.categories {
    out.write_string(
      "\{group.category}: \{group.custom_ordinals.length()} instances, \{group.total_bytes} B\n",
    )
  }
  out.write_string(
    "\nFULL-NAME SUMMARY (unavailable is a separate accounting bucket)\n",
  )
  for group in report.name_groups {
    out.write_string(
      path_optional_string(group.name).stringify() +
      "  \{group.custom_ordinals.length()} instances  \{group.total_bytes} B\n",
    )
  }
  let customs_shown = top.max(0).min(report.customs.length())
  out.write_string(
    "\nCUSTOM INSTANCES: showing \{customs_shown} of \{report.customs.length()}\n",
  )
  for i in 0.. n.to_string()
          None => "unavailable"
        }) +
        "; count prefix \{sub.count_prefix_bytes} B; parsed records \{sub.parsed_record_bytes} B; unparsed " +
        sub.unparsed.map(metadata_range_text).unwrap_or("none") +
        "\n",
      )
    }
  }
  let records_shown = top.max(0).min(report.function_names.length())
  out.write_string(
    "\nFUNCTION-NAME RECORDS: showing \{records_shown} of \{report.function_names.length()} (encoded size descending)\n",
  )
  out.write_string(
    "CUSTOM  SUB  RECORD  DECLARED INDEX  ENCODED B  INDEX PREFIX B  NAME PREFIX B  UTF8 B  FILE OFFSET  NAME\n",
  )
  for i in 0..