///|
fn locals_type_totals_json(totals : FunctionLocalsTypeTotals) -> Json {
  let value : Map[String, Json] = Map([])
  value.set("group_indices", Json::array(totals.group_indices.map(int_json)))
  value.set("group_count", int_json(totals.group_count))
  value.set(
    "declared_count_decimal",
    Json::string(totals.declared_count.to_string()),
  )
  value.set("quantity_encoding_bytes", int_json(totals.quantity_encoding_bytes))
  value.set("type_encoding_bytes", int_json(totals.type_encoding_bytes))
  value.set("group_bytes", int_json(totals.group_bytes))
  Json::object(value)
}

///|
pub fn render_function_locals_comparison_json(
  report : FunctionLocalsComparison,
) -> String {
  let root : Map[String, Json] = Map([])
  root.set("format", Json::string("moonsize-function-locals-diff"))
  root.set("schema_version", int_json(1))
  root.set("baseline", function_locals_json_value(report.baseline))
  root.set("current", function_locals_json_value(report.current))
  let d = report.delta
  let delta : Map[String, Json] = Map([])
  for
    (key, value) in [
      ("entry_bytes", d.entry_bytes),
      ("prefix_bytes", d.prefix_bytes),
      ("body_bytes", d.body_bytes),
      ("locals_bytes", d.locals_bytes),
      ("group_count_encoding_bytes", d.group_count_encoding_bytes),
      ("quantity_encoding_bytes", d.quantity_encoding_bytes),
      ("type_encoding_bytes", d.type_encoding_bytes),
      ("body_remainder_bytes", d.body_remainder_bytes),
      ("group_count", d.group_count),
    ] {
    delta.set(key, int_json(value))
  }
  delta.set(
    "declared_count_delta_decimal",
    Json::string(d.declared_count_delta_decimal),
  )
  root.set("delta", Json::object(delta))
  root.set(
    "type_encodings",
    Json::array(
      report.type_encodings.map(fn(row) {
        let value : Map[String, Json] = Map([])
        value.set("type_hex", Json::string(row.type_hex))
        value.set("type_label", path_optional_string(row.type_label))
        value.set("presence", Json::string(row.presence))
        value.set("baseline", locals_type_totals_json(row.baseline))
        value.set("current", locals_type_totals_json(row.current))
        let delta : Map[String, Json] = Map([])
        delta.set("group_count", int_json(row.delta.group_count))
        delta.set(
          "declared_count_delta_decimal",
          Json::string(row.delta.declared_count_delta_decimal),
        )
        delta.set(
          "quantity_encoding_bytes",
          int_json(row.delta.quantity_encoding_bytes),
        )
        delta.set(
          "type_encoding_bytes",
          int_json(row.delta.type_encoding_bytes),
        )
        delta.set("group_bytes", int_json(row.delta.group_bytes))
        value.set("delta", Json::object(delta))
        Json::object(value)
      }),
    ),
  )
  root.set("scope", Json::string(report.scope))
  Json::object(root).stringify(indent=2) + "\n"
}

///|
pub fn render_function_locals_comparison(
  report : FunctionLocalsComparison,
  top : Int,
) -> String {
  let out = StringBuilder()
  let b = report.baseline
  let c = report.current
  let d = report.delta
  out.write_string(
    "FUNCTION LOCAL DECLARATIONS COMPARISON\n\nExplicit user-selected targets; no automatic function matching.\n",
  )
  for (side, target) in [("Baseline", b), ("Current", c)] {
    out.write_string(
      "\n\{side}: function \{target.target.index} " +
      search_terminal_string(target.target.name) +
      "; SHA-256 \{target.sha256}; input \{target.file_size} B\n",
    )
    out.write_string(
      "Entry: file offset \{target.entry.file_offset}, size \{target.entry.size} B\nLocals: file offset \{target.locals_encoding.file_offset}, size \{target.locals_encoding.size} B\nBody remainder (undecoded): file offset \{target.body_remainder.file_offset}, size \{target.body_remainder.size} B\n",
    )
  }
  out.write_string(
    "\nBYTE AND COUNT LEDGER (delta = current - baseline)\nMETRIC  BASELINE  CURRENT  DELTA\n",
  )
  for
    (label, before, after, change) in [
      ("Entry bytes", b.entry.size, c.entry.size, d.entry_bytes),
      ("Prefix bytes", b.prefix.size, c.prefix.size, d.prefix_bytes),
      ("Body bytes", b.body.size, c.body.size, d.body_bytes),
      (
        "Locals bytes",
        b.locals_encoding.size,
        c.locals_encoding.size,
        d.locals_bytes,
      ),
      (
        "Group-count encoding bytes",
        b.group_count_encoding.size,
        c.group_count_encoding.size,
        d.group_count_encoding_bytes,
      ),
      (
        "Quantity encoding bytes",
        b.quantity_encoding_bytes,
        c.quantity_encoding_bytes,
        d.quantity_encoding_bytes,
      ),
      (
        "Type encoding bytes",
        b.type_encoding_bytes,
        c.type_encoding_bytes,
        d.type_encoding_bytes,
      ),
      (
        "Undecoded remainder bytes",
        b.body_remainder.size,
        c.body_remainder.size,
        d.body_remainder_bytes,
      ),
      ("Group count", b.group_count, c.group_count, d.group_count),
    ] {
    out.write_string("\{label}  \{before}  \{after}  \{change}\n")
  }
  out.write_string(
    "Declared count (excluding parameters)  \{b.declared_count}  \{c.declared_count}  \{d.declared_count_delta_decimal}\n",
  )
  out.write_string(
    "Entry delta: \{d.entry_bytes} = prefix \{d.prefix_bytes} + body \{d.body_bytes}\nBody delta: \{d.body_bytes} = locals \{d.locals_bytes} + undecoded remainder \{d.body_remainder_bytes}\nLocals delta: \{d.locals_bytes} = group count encoding \{d.group_count_encoding_bytes} + quantity encoding \{d.quantity_encoding_bytes} + type encoding \{d.type_encoding_bytes}\n",
  )
  let shown = top.max(0).min(report.type_encodings.length())
  out.write_string(
    "\nTYPE ENCODINGS: showing \{shown} of \{report.type_encodings.length()} (full hex lexical order)\nTYPE HEX  LABEL  PRESENCE  BEFORE GROUPS  AFTER GROUPS  GROUP DELTA  BEFORE QUANTITY  AFTER QUANTITY  QUANTITY DELTA  BEFORE B  AFTER B  BYTE DELTA\n",
  )
  if report.type_encodings.is_empty() {
    out.write_string("No declaration groups on either side.\n")
  }
  for i in 0.. label
        None => "unknown"
      }) +
      "  " +
      row.presence +
      "  \{row.baseline.group_count}  \{row.current.group_count}  \{row.delta.group_count}  \{row.baseline.declared_count}  \{row.current.declared_count}  \{row.delta.declared_count_delta_decimal}  \{row.baseline.group_bytes}  \{row.current.group_bytes}  \{row.delta.group_bytes}\n",
    )
    out.write_string(
      "  baseline group indices: " +
      Json::array(row.baseline.group_indices.map(int_json)).stringify() +
      "; current group indices: " +
      Json::array(row.current.group_indices.map(int_json)).stringify() +
      "\n",
    )
    out.write_string(
      "  quantity encoding B: \{row.baseline.quantity_encoding_bytes} -> \{row.current.quantity_encoding_bytes} (delta \{row.delta.quantity_encoding_bytes}); type encoding B: \{row.baseline.type_encoding_bytes} -> \{row.current.type_encoding_bytes} (delta \{row.delta.type_encoding_bytes})\n",
    )
  }
  out.write_string("\n" + report.scope + "\n")
  out.to_string()
}