// Chunk disassembler: renders a chunk's `code` array as human-readable text.
// Used for debug logging, snapshot tests, and by developers running the CLI
// with `--dump-bytecode`.
//
// Format: one instruction per (logical) line
//   `NNNN: OPNAME [operand]   ; line:col`
// Wide-encoded pairs render as one logical line whose operand shows the merged
// 32-bit value. Unknown opcodes appear as ``.

///|
/// Instructions carrying an unsigned operand (constant-pool index, local slot
/// index, etc.). Determines whether the disassembler expects an operand.
fn is_unsigned_operand_op(op : Byte) -> Bool {
  op == OP_PUSH_CONST ||
  op == OP_GET_LOCAL ||
  op == OP_SET_LOCAL ||
  op == OP_GET_UPVALUE ||
  op == OP_SET_UPVALUE ||
  op == OP_GET_GLOBAL ||
  op == OP_SET_GLOBAL ||
  op == OP_DECLARE_GLOBAL ||
  op == OP_GET_GLOBAL_OR_UNDEF ||
  op == OP_DEFINE_PROP ||
  op == OP_GET_PROP ||
  op == OP_SET_PROP ||
  op == OP_DELETE_PROP ||
  op == OP_NEW_ARRAY ||
  op == OP_NEW_CLOSURE ||
  op == OP_CALL ||
  op == OP_CALL_METHOD ||
  op == OP_CONSTRUCT ||
  op == OP_ENTER_TRY
}

///|
/// Instructions carrying a signed operand (inline i32 literal, jump offsets).
fn is_signed_operand_op(op : Byte) -> Bool {
  op == OP_PUSH_I32 ||
  op == OP_JUMP ||
  op == OP_JUMP_IF_TRUE ||
  op == OP_JUMP_IF_FALSE
}

///|
/// Pad `s` on the right with spaces up to `width` for aligned column output.
/// Used to keep opcode name + operand columns lined up in the disassembly.
fn pad_right(s : String, width : Int) -> String {
  if s.length() >= width {
    s
  } else {
    let sb = StringBuilder::StringBuilder(size_hint=width)
    sb.write_string(s)
    for _ in 0..<(width - s.length()) {
      sb.write_char(' ')
    }
    sb.to_string()
  }
}

///|
/// Left-pad `n` with zeros to width `width`. Used for the PC column so a
/// chunk of any size renders with fixed-width offsets.
fn pad_pc(n : Int, width : Int) -> String {
  let s = n.to_string()
  if s.length() >= width {
    s
  } else {
    let sb = StringBuilder::StringBuilder(size_hint=width)
    for _ in 0..<(width - s.length()) {
      sb.write_char('0')
    }
    sb.write_string(s)
    sb.to_string()
  }
}

///|
/// Human-readable disassembly of the chunk. One line per logical instruction
/// (a wide-encoded operand still renders as a single line). Trailing newline
/// is included so appending disassembler output to a growing log is clean.
///
/// This is a debug helper, not a stability contract — the exact spacing and
/// column widths may change to accommodate longer opcode names in later
/// milestones. Snapshot tests should be updated when the format shifts.
pub fn Chunk::disassemble(self : Chunk) -> String {
  let sb = StringBuilder::StringBuilder(size_hint=self.code.length() * 32)
  // First line: chunk header — name and filename for context.
  sb.write_string("== ")
  sb.write_string(self.name)
  sb.write_string(" (")
  sb.write_string(self.filename)
  sb.write_string(") ==\n")
  let mut pc = 0
  while pc < self.code.length() {
    let decoded = decode(self.code[pc])
    let display_pc = pc
    let name = opcode_name(decoded.op)
    // Compute operand + advance for opcodes that carry one; also detect wide.
    let (operand_str, advance) = if decoded.op == OP_WIDE {
      // Wide always precedes an operand-bearing opcode. Read via the same
      // helper that the VM uses so the two views stay in sync.
      if pc + 1 >= self.code.length() {
        // Malformed: dangling wide. Render it as a bare WIDE line.
        ("", 1)
      } else {
        let follow_op = decode(self.code[pc + 1]).op
        if is_signed_operand_op(follow_op) {
          let (v, adv) = self.read_operand_i24(pc)
          (v.to_string(), adv)
        } else {
          let (v, adv) = self.read_operand_u24(pc)
          (v.to_string(), adv)
        }
      }
    } else if is_signed_operand_op(decoded.op) {
      let (v, adv) = self.read_operand_i24(pc)
      (v.to_string(), adv)
    } else if is_unsigned_operand_op(decoded.op) {
      let (v, adv) = self.read_operand_u24(pc)
      (v.to_string(), adv)
    } else {
      ("", 1)
    }

    // When wide, the *logical* opcode name comes from the following op.
    let logical_name = if decoded.op == OP_WIDE && pc + 1 < self.code.length() {
      opcode_name(decode(self.code[pc + 1]).op) + " (wide)"
    } else {
      name
    }
    let loc = self.source_locs[pc]
    sb.write_string(pad_pc(display_pc, 4))
    sb.write_string(": ")
    // Opcode name column: 30 chars accommodates "JUMP_IF_FALSE (wide)" plus a
    // multi-digit operand while leaving a couple of spaces before the `; loc`.
    let op_col = if operand_str == "" {
      logical_name
    } else {
      logical_name + " " + operand_str
    }
    sb.write_string(pad_right(op_col, 30))
    sb.write_string(" ; ")
    sb.write_string(loc.line.to_int().to_string())
    sb.write_string(":")
    sb.write_string(loc.col.to_int().to_string())
    sb.write_string("\n")
    pc = pc + advance
  }
  sb.to_string()
}