#!/usr/bin/env python3
"""
Generate a synthetic EE-RAM image + manifest for Ch270's ELF runner TB.

Produces two files at the requested output prefix:

  <prefix>.image.hex     iverilog $readmemh compatible. Uses @<hex_qw_idx>
                          directives so only the populated 128-bit qwords
                          appear (the TB pre-zeros the array before reading).
                          Each line is 32 hex chars = one 128-bit qword,
                          MSB-first (byte 15 leftmost, byte 0 rightmost).

  <prefix>.manifest.hex  Two lines:
                            line 0: ELF entry point (32-bit hex)
                            line 1: stack-top hint (32-bit hex; unused
                                     by current TB but reserved)

The synthetic program lives at PHYS 0x00100000. Entry is given as a
kseg0 address (0x80100008) because the ee_memory_map stub routes
useg (top bit = 0) to a separate useg_shadow region, not ee_ram —
real PS2 ELFs use kseg0 entries for the same reason (cached text):

    PHYS 0x00100000  /  kseg0 0x80100000:  nop      pad
    PHYS 0x00100004  /  kseg0 0x80100004:  nop      pad
    PHYS 0x00100008  /  kseg0 0x80100008:  addiu $v0,$0,0x1234   *** entry ***
    PHYS 0x0010000C  /  kseg0 0x8010000C:  addiu $v1,$0,0x5678
    PHYS 0x00100010  /  kseg0 0x80100010:  j 0x80100010          loop-to-self
    PHYS 0x00100014  /  kseg0 0x80100014:  nop                    delay slot

The J encoding (0x08040004) is PC-relative: at runtime, j_tgt =
{PC+4[31:28], imm26<<2}, so the high 4 bits come from the PC.
PC=0x80100010 ⇒ j_tgt = 0x80100010 (self) — same encoding works for
both kseg0 and kuseg views.

Expected TB verdict: `elf_timeout_with_hot_pc` with hot_pc near
0x80100010. That confirms the ELF-load + entry-bootstrap + strict-
trace pipeline is sound (no traps, no halts, no unmapped MMIO, EE
reaches and executes real code).
"""

import sys
import struct
import argparse


def encode_addiu(rt: int, rs: int, imm: int) -> int:
    """ADDIU rt, rs, imm.  op=0x09."""
    return (0x09 << 26) | ((rs & 0x1F) << 21) | ((rt & 0x1F) << 16) | (imm & 0xFFFF)


def encode_j(target: int) -> int:
    """J target.  op=0x02. Target must be word-aligned."""
    assert target & 3 == 0, "J target must be word-aligned"
    return (0x02 << 26) | ((target >> 2) & 0x03FFFFFF)


def encode_lui(rt: int, imm: int) -> int:
    """LUI rt, imm.  op=0x0F."""
    return (0x0F << 26) | ((rt & 0x1F) << 16) | (imm & 0xFFFF)


def encode_ori(rt: int, rs: int, imm: int) -> int:
    """ORI rt, rs, imm.  op=0x0D."""
    return (0x0D << 26) | ((rs & 0x1F) << 21) | ((rt & 0x1F) << 16) | (imm & 0xFFFF)


def encode_jr(rs: int) -> int:
    """JR rs.  SPECIAL/funct=0x08."""
    return ((rs & 0x1F) << 21) | 0x08


def write_word_le(image: bytearray, phys_addr: int, word: int) -> None:
    """Write a 32-bit word little-endian into the EE-RAM image."""
    assert phys_addr + 4 <= len(image), "phys_addr out of image bounds"
    image[phys_addr + 0] = (word >> 0)  & 0xFF
    image[phys_addr + 1] = (word >> 8)  & 0xFF
    image[phys_addr + 2] = (word >> 16) & 0xFF
    image[phys_addr + 3] = (word >> 24) & 0xFF


def qword_to_hex(image: bytearray, qw_phys: int) -> str:
    """Return the 32-char hex string for the qword at byte offset qw_phys.

    iverilog $readmemh expects the leftmost hex char to be the highest
    bit of the 128-bit value. Byte 15 is the most significant byte;
    byte 0 is the least.
    """
    assert qw_phys + 16 <= len(image)
    bytes16 = image[qw_phys:qw_phys + 16]
    # Reverse to MSB-first for the hex string.
    return bytes16[::-1].hex()


def emit_image_hex(image: bytearray, path: str, qw_size: int) -> None:
    """Emit a $readmemh-compatible hex file using @<idx> directives for
    every populated (non-zero) qword. Empty qwords are skipped — the TB
    pre-zeros the array before reading.
    """
    with open(path, "w") as f:
        f.write("// Ch270 synthetic EE-RAM image\n")
        f.write(f"// {len(image)} bytes / {len(image)//qw_size} qwords\n")
        f.write("// Populated qwords only; TB zero-inits before $readmemh.\n\n")
        any_emitted = False
        for qw_idx in range(0, len(image) // qw_size):
            qw_byte = qw_idx * qw_size
            qw_bytes = image[qw_byte:qw_byte + qw_size]
            if any(b != 0 for b in qw_bytes):
                f.write(f"@{qw_idx:08x}\n")
                f.write(qword_to_hex(image, qw_byte) + "\n")
                any_emitted = True
        if not any_emitted:
            # iverilog $readmemh errors on empty file; emit a benign entry.
            f.write("@00000000\n00000000000000000000000000000000\n")


def emit_manifest_hex(path: str, entry: int, stack_top: int) -> None:
    """Emit the manifest as two 32-bit hex lines."""
    with open(path, "w") as f:
        f.write("// Ch270 manifest\n")
        f.write(f"// line 0 = entry, line 1 = stack_top hint\n")
        f.write(f"{entry:08x}\n")
        f.write(f"{stack_top:08x}\n")


def build_synthetic_image(image_bytes: int, entry_phys: int) -> bytearray:
    """Build the EE-RAM image with the synthetic program at entry_phys."""
    image = bytearray(image_bytes)
    # Pad before entry so PC starts on real instructions:
    write_word_le(image, entry_phys - 8, 0x00000000)            # nop
    write_word_le(image, entry_phys - 4, 0x00000000)            # nop
    # Body:
    write_word_le(image, entry_phys + 0,  encode_addiu(2, 0, 0x1234))   # $v0 = 0x1234
    write_word_le(image, entry_phys + 4,  encode_addiu(3, 0, 0x5678))   # $v1 = 0x5678
    write_word_le(image, entry_phys + 8,  encode_j(entry_phys + 8))     # j self
    write_word_le(image, entry_phys + 12, 0x00000000)                   # nop delay slot
    return image


def main() -> int:
    p = argparse.ArgumentParser(description=__doc__,
                                formatter_class=argparse.RawDescriptionHelpFormatter)
    p.add_argument("--out-prefix", required=True,
                   help="output file prefix (writes <prefix>.image.hex + <prefix>.manifest.hex)")
    p.add_argument("--entry", type=lambda s: int(s, 0), default=0x80100008,
                   help="entry point VIRTUAL address (kseg0 default 0x80100008; "
                        "physical placement of the code segment is entry & 0x1FFFFFFF)")
    p.add_argument("--ee-ram-bytes", type=lambda s: int(s, 0), default=2 * 1024 * 1024,
                   help="EE RAM size in bytes (default 2 MiB; must be >= entry+16)")
    p.add_argument("--stack-top", type=lambda s: int(s, 0), default=0x801FFFF0,
                   help="stack top hint stored in manifest (default 0x801FFFF0 kseg0)")
    args = p.parse_args()

    entry_phys = args.entry & 0x1FFFFFFF
    if entry_phys < 8 or entry_phys + 16 > args.ee_ram_bytes:
        p.error(f"entry 0x{args.entry:08x} (phys 0x{entry_phys:08x}) "
                f"doesn't fit into 0x{args.ee_ram_bytes:x}-byte EE RAM")

    image = build_synthetic_image(args.ee_ram_bytes, entry_phys)
    emit_image_hex(image, f"{args.out_prefix}.image.hex", qw_size=16)
    emit_manifest_hex(f"{args.out_prefix}.manifest.hex",
                      entry=args.entry, stack_top=args.stack_top)
    print(f"[generate_synthetic_image] wrote {args.out_prefix}.image.hex "
          f"+ {args.out_prefix}.manifest.hex (entry=0x{args.entry:08x}, "
          f"phys=0x{entry_phys:08x}, ee_ram={args.ee_ram_bytes} bytes)")
    return 0


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