///|
fn signed_bytes(delta : Int) -> String {
  (if delta > 0 { "+" } else { "" }) + delta.to_string()
}

///|
fn diff_numbers(baseline : Int, current : Int) -> String {
  pad_left(baseline.to_string(), 12) +
  "  " +
  pad_left(current.to_string(), 12) +
  "  " +
  pad_left(signed_bytes(current - baseline), 12)
}

///|
fn diff_index(stat : FunctionStat?) -> String {
  match stat {
    Some(stat) => stat.index.to_string()
    None => "-"
  }
}

///|
fn coverage_line(
  name : String,
  before : FunctionCoverage,
  after : FunctionCoverage,
) -> String {
  "  " +
  pad_right(name, 16) +
  "  " +
  pad_left(before.count.to_string(), 10) +
  "  " +
  pad_left(after.count.to_string(), 10) +
  "  " +
  pad_left(before.bytes.to_string(), 12) +
  "  " +
  pad_left(after.bytes.to_string(), 12) +
  "\n"
}

///|
/// Text-only first release. Ranking uses absolute delta then exact symbol/package
/// identity; indices locate records within a build and never establish a match.
/// top=0 hides both detail rankings while retaining all totals and coverage.
pub fn render_function_diff(diff : ModuleSizeDiff, top : Int) -> String {
  let out = StringBuilder()
  out.write_string("FUNCTION SIZE CHANGES\n\n")
  out.write_string(
    "  Encoded bytes = body length prefix + locals + instructions.\n",
  )
  out.write_string(
    "  " +
    pad_right("TOTAL", 16) +
    "  " +
    pad_left("BASELINE", 12) +
    "  " +
    pad_left("CURRENT", 12) +
    "  " +
    pad_left("DELTA", 12) +
    "\n",
  )
  out.write_string(
    "  " +
    pad_right("file", 16) +
    "  " +
    diff_numbers(diff.baseline_size, diff.current_size) +
    "\n",
  )
  out.write_string(
    "  " +
    pad_right("function encoding", 16) +
    "  " +
    diff_numbers(
      diff.baseline_code.function_bytes,
      diff.current_code.function_bytes,
    ) +
    "\n",
  )
  if top > 0 {
    out.write_string(
      "\n  " +
      pad_right("STATUS", 14) +
      "  " +
      pad_left("BASE_INDEX", 10) +
      "  " +
      pad_left("CURRENT_INDEX", 13) +
      "  " +
      pad_left("BASE_ENCODED", 12) +
      "  " +
      pad_left("CUR_ENCODED", 12) +
      "  " +
      pad_left("DELTA", 12) +
      "  RAW_SYMBOL\n",
    )
    for i, row in diff.functions {
      if i >= top {
        break
      }
      let symbol = match row.raw_name {
        Some(raw) => Json::string(raw).stringify()
        None => ""
      }
      out.write_string(
        "  " +
        pad_right(change_status_text(row.status), 14) +
        "  " +
        pad_left(diff_index(row.baseline), 10) +
        "  " +
        pad_left(diff_index(row.current), 13) +
        "  " +
        diff_numbers(stat_size(row.baseline), stat_size(row.current)) +
        "  " +
        symbol +
        "\n",
      )
    }
    if diff.functions.is_empty() {
      out.write_string("  no local functions\n")
    }
  } else {
    out.write_string("\n  detail rankings hidden (--top 0)\n")
  }
  out.write_string(
    "\nPACKAGE SIZE CHANGES\n\n  Best-effort symbol package attribution; includes unmatched functions.\n",
  )
  out.write_string(
    "  package totals: " +
    diff_numbers(
      diff.baseline_code.function_bytes,
      diff.current_code.function_bytes,
    ) +
    "\n",
  )
  if top > 0 {
    out.write_string(
      "\n  " +
      pad_left("BASE_FUNCS", 10) +
      "  " +
      pad_left("CURRENT_FUNCS", 13) +
      "  " +
      pad_left("BASE_ENCODED", 12) +
      "  " +
      pad_left("CUR_ENCODED", 12) +
      "  " +
      pad_left("DELTA", 12) +
      "  PACKAGE\n",
    )
    for i, row in diff.packages {
      if i >= top {
        break
      }
      out.write_string(
        "  " +
        pad_left(row.baseline_functions.to_string(), 10) +
        "  " +
        pad_left(row.current_functions.to_string(), 13) +
        "  " +
        diff_numbers(row.baseline_size, row.current_size) +
        "  " +
        Json::string(row.name).stringify() +
        "\n",
      )
    }
    if diff.packages.is_empty() {
      out.write_string("  no packages with local functions\n")
    }
  }
  let summary = diff.matching
  let zero : FunctionCoverage = { count: 0, bytes: 0, }
  out.write_string(
    "\nMATCHING SUMMARY\n\n  " +
    pad_right("CATEGORY", 16) +
    "  " +
    pad_left("BASE_COUNT", 10) +
    "  " +
    pad_left("CUR_COUNT", 10) +
    "  " +
    pad_left("BASE_BYTES", 12) +
    "  " +
    pad_left("CUR_BYTES", 12) +
    "\n",
  )
  out.write_string(
    coverage_line("matched", summary.matched_baseline, summary.matched_current),
  )
  out.write_string(coverage_line("added-symbol", zero, summary.added_symbols))
  out.write_string(
    coverage_line("removed-symbol", summary.removed_symbols, zero),
  )
  out.write_string(
    coverage_line(
      "ambiguous",
      summary.ambiguous_baseline,
      summary.ambiguous_current,
    ),
  )
  out.write_string(
    coverage_line(
      "missing-symbol",
      summary.missing_baseline,
      summary.missing_current,
    ),
  )
  out.write_string(
    "\n  Matched coverage: baseline " +
    share_text(
      summary.matched_baseline.count,
      diff.baseline_code.function_count,
    ) +
    " of functions / " +
    share_text(
      summary.matched_baseline.bytes,
      diff.baseline_code.function_bytes,
    ) +
    " of encoded bytes; current " +
    share_text(summary.matched_current.count, diff.current_code.function_count) +
    " of functions / " +
    share_text(summary.matched_current.bytes, diff.current_code.function_bytes) +
    " of encoded bytes.\n",
  )
  let unmatched_before = summary.ambiguous_baseline.bytes +
    summary.missing_baseline.bytes
  let unmatched_after = summary.ambiguous_current.bytes +
    summary.missing_current.bytes
  if summary.ambiguous_baseline.count +
    summary.ambiguous_current.count +
    summary.missing_baseline.count +
    summary.missing_current.count >
    0 {
    out.write_string(
      "  WARNING: ambiguous or missing symbols prevent function identity matching; unmatched bytes remain in package totals.\n",
    )
  }
  out.write_string(
    "  Only symbols unique on both sides are matched. Added/removed symbols may reflect renames or toolchain changes, not source additions/deletions.\n",
  )
  out.write_string(
    "  Function delta = matched " +
    signed_bytes(summary.matched_delta) +
    " + added " +
    summary.added_symbols.bytes.to_string() +
    " - removed " +
    summary.removed_symbols.bytes.to_string() +
    " + unmatched current " +
    unmatched_after.to_string() +
    " - unmatched baseline " +
    unmatched_before.to_string() +
    " = " +
    signed_bytes(
      diff.current_code.function_bytes - diff.baseline_code.function_bytes,
    ) +
    " B.\n",
  )
  out.write_string(
    "\nCODE OVERHEAD\n\n  " +
    pad_right("RAW BYTES", 16) +
    "  " +
    pad_left("BASELINE", 12) +
    "  " +
    pad_left("CURRENT", 12) +
    "  " +
    pad_left("DELTA", 12) +
    "\n",
  )
  for
    (label, before, after) in [
      (
        "code section",
        diff.baseline_code.code_size,
        diff.current_code.code_size,
      ),
      (
        "function encoding",
        diff.baseline_code.function_bytes,
        diff.current_code.function_bytes,
      ),
      ("code overhead", diff.baseline_code.overhead, diff.current_code.overhead),
    ] {
    out.write_string(
      "  " + pad_right(label, 16) + "  " + diff_numbers(before, after) + "\n",
    )
  }
  out.write_string(
    "\n  Code overhead includes the section header and function-count encoding.\n",
  )
  out.write_string(
    "  Function changes do not sum to file changes; see the existing section comparison for name, data and other sections.\n",
  )
  out.to_string()
}