///|
fn function_call_kind_text(kind : FunctionCallKind) -> String {
  match kind {
    DirectCall => "direct"
    IndirectCall => "indirect"
    ReferenceCall => "reference_call"
  }
}

///|
pub fn render_function_calls(report : FunctionCallsReport, top : Int) -> String {
  let out = StringBuilder()
  out.write_string(
    "FUNCTION CALL SITES\n\nCaller: #\{report.caller.index} " +
    search_terminal_string(report.caller.name) +
    " (local)\nInput: \{report.file_size} B; SHA-256 \{report.sha256}\n",
  )
  out.write_string(
    "Entry: file offset \{report.entry.file_offset}, size \{report.entry.size} B\nBody: file offset \{report.body.file_offset}, size \{report.body.size} B\n",
  )
  out.write_string(
    "Encoded bytes: \{report.entry.size} B = prefix \{report.prefix.size} B + body \{report.body.size} B\nBody bytes: \{report.body.size} B = locals \{report.locals_encoding.size} B + instructions \{report.instruction_bytes} B\n",
  )
  out.write_string(
    "Instructions: \{report.instruction_count}\nCalls: \{report.call_count} = direct \{report.direct_count} + indirect \{report.indirect_count} + reference_call \{report.reference_call_count}\nTail calls: \{report.tail_count} (subset)\nInstruction bytes: \{report.instruction_bytes} B = call encodings \{report.call_encoding_bytes} B + other instructions \{report.other_instruction_bytes} B\n",
  )
  let shown = top.max(0).min(report.calls.length())
  out.write_string(
    "\nCALLS: showing \{shown} of \{report.call_count}\nORDINAL  OPCODE  KIND  TAIL  FILE OFFSET  BODY OFFSET  SIZE  TARGET  TYPE  TABLE\n",
  )
  if report.calls.is_empty() {
    out.write_string("No call instructions in this local function.\n")
  }
  for i in 0.. "unknown"
      Some(node) =>
        "#\{node.index} " +
        search_terminal_string(node.name) +
        (if node.imported {
          " (import " +
          search_terminal_string(node.import_module.unwrap()) +
          "." +
          search_terminal_string(node.import_name.unwrap()) +
          ")"
        } else {
          " (local)"
        })
    }
    out.write_string(
      "\{call.instruction_ordinal}  " +
      call.opcode.name() +
      "  " +
      function_call_kind_text(call.kind) +
      "  " +
      (if call.is_tail { "yes" } else { "no" }) +
      "  \{call.file_offset}  \{call.body_relative_offset}  \{call.encoded_size}  " +
      target +
      "  " +
      (match call.type_index {
        Some(index) => index.to_string()
        None => "n/a"
      }) +
      "  " +
      (match call.table_index {
        Some(index) => index.to_string()
        None => "n/a"
      }) +
      "\n",
    )
  }
  out.write_string("\n" + report.scope + "\n")
  out.to_string()
}

///|
pub fn render_function_calls_json(report : FunctionCallsReport) -> String {
  let root : Map[String, Json] = Map([])
  root.set("format", Json::string("moonsize-function-calls"))
  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("caller", path_node_json(report.caller))
  root.set("entry", function_instruction_range_json(report.entry))
  root.set("prefix", function_instruction_range_json(report.prefix))
  root.set("body", function_instruction_range_json(report.body))
  root.set(
    "locals_encoding",
    function_instruction_range_json(report.locals_encoding),
  )
  root.set("instruction_count", int_json(report.instruction_count))
  root.set("instruction_bytes", int_json(report.instruction_bytes))
  root.set("call_count", int_json(report.call_count))
  root.set("direct_count", int_json(report.direct_count))
  root.set("indirect_count", int_json(report.indirect_count))
  root.set("reference_call_count", int_json(report.reference_call_count))
  root.set("tail_count", int_json(report.tail_count))
  root.set("call_encoding_bytes", int_json(report.call_encoding_bytes))
  root.set("other_instruction_bytes", int_json(report.other_instruction_bytes))
  root.set(
    "calls",
    Json::array(
      report.calls.map(fn(call) {
        let row : Map[String, Json] = Map([])
        row.set("instruction_ordinal", int_json(call.instruction_ordinal))
        row.set("opcode", Json::string(call.opcode.name()))
        row.set("kind", Json::string(function_call_kind_text(call.kind)))
        row.set("is_tail", Json::boolean(call.is_tail))
        row.set("file_offset", int_json(call.file_offset))
        row.set("body_relative_offset", int_json(call.body_relative_offset))
        row.set("encoded_size", int_json(call.encoded_size))
        row.set("target_index", optional_index_json(call.target_index))
        row.set("type_index", optional_index_json(call.type_index))
        row.set("table_index", optional_index_json(call.table_index))
        row.set(
          "target",
          match call.target {
            Some(node) => path_node_json(node)
            None => Json::null()
          },
        )
        Json::object(row)
      }),
    ),
  )
  root.set("scope", Json::string(report.scope))
  Json::object(root).stringify(indent=2) + "\n"
}