#!/usr/bin/env python3
# retroDE_ps2 — bin_to_hex
#
# Convert a raw 32-bit binary image (e.g. a PS2 BIOS dump) to the text
# hex format iverilog's $readmemh expects: one 32-bit word per line,
# hex (no 0x prefix), newline-terminated. iverilog reads the first line
# into memory index 0 and advances by one per line.
#
# Typical usage:
#   python3 sim/golden/bin_to_hex.py path/to/ps2_bios.bin > /tmp/bios.hex
#   make -C sim tb_iop_core_bios_smoke BIOS=/tmp/bios.hex
#
# Endianness:
#   PS2 BIOS images on disk are little-endian byte order, which matches
#   how the core will see each 32-bit word once it's fetched from the
#   bios_rom_stub backing memory. We emit words using `<I` (little-
#   endian unsigned 32-bit) to preserve that.
#
# Image size:
#   Inputs shorter than the BIOS ROM are padded to the stub's
#   DEFAULT_SIZE_WORDS (1 Mi words = 4 MiB) so $readmemh fills the whole
#   array and leaves no stale values. Inputs longer than that are
#   rejected — they're either the wrong file or would silently trample
#   something.
#
# See sim/README.md "Real-BIOS iteration" section for the full workflow.

import argparse
import struct
import sys

DEFAULT_SIZE_WORDS = 1 << 20   # 4 MiB / 4 bytes


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Convert a binary BIOS image to hex-text for $readmemh.")
    parser.add_argument("input", help="input .bin file")
    parser.add_argument(
        "-o", "--output",
        help="output .hex path (default: stdout)")
    parser.add_argument(
        "--words", type=int, default=DEFAULT_SIZE_WORDS,
        help=f"total words to emit (default {DEFAULT_SIZE_WORDS}, the "
             f"bios_rom_stub default depth)")
    parser.add_argument(
        "--pad", default="00000000",
        help="pad word used when input is shorter than --words "
             "(default 00000000 = SLL $0,$0,0 = NOP)")
    args = parser.parse_args()

    with open(args.input, "rb") as fh:
        raw = fh.read()

    if len(raw) % 4 != 0:
        print(
            f"warning: input length {len(raw)} not a multiple of 4; "
            "trailing bytes dropped",
            file=sys.stderr)
        raw = raw[: len(raw) - (len(raw) % 4)]

    words = list(struct.iter_unpack("<I", raw))
    n_input = len(words)

    if n_input > args.words:
        print(
            f"error: input has {n_input} words but only {args.words} "
            "will be emitted; refusing to truncate",
            file=sys.stderr)
        return 2

    out_fh = open(args.output, "w") if args.output else sys.stdout
    try:
        for (w,) in words:
            out_fh.write(f"{w:08x}\n")
        pad_word = int(args.pad, 16)
        for _ in range(args.words - n_input):
            out_fh.write(f"{pad_word:08x}\n")
    finally:
        if args.output:
            out_fh.close()

    print(
        f"[bin_to_hex] wrote {args.words} words "
        f"({n_input} from input, {args.words - n_input} pad) "
        f"to {args.output or 'stdout'}",
        file=sys.stderr)
    return 0


if __name__ == "__main__":
    sys.exit(main())
