#!/usr/bin/env python3
"""
Convert a PS2 EE ELF into a tb_ee_core_elf_runner-compatible image
manifest pair (no external deps; pure stdlib).

Emits two files at the requested output prefix, identical in format
to generate_synthetic_image.py:

  <prefix>.image.hex     iverilog $readmemh, @<qw_idx> directives for
                          populated 128-bit qwords only. Each line is
                          32 hex chars (MSB-first, byte 15 leftmost).

  <prefix>.manifest.hex  line 0 = ELF entry point (32-bit hex)
                         line 1 = stack-top hint (32-bit hex)

Supports ELF32 little-endian, ELFCLASS32, EM_MIPS, e_type ET_EXEC or
ET_DYN. PT_LOAD segments are placed at their physical address
(low 29 bits of p_vaddr — strips the kuseg/kseg0/kseg1 alias bits so
the data lands at the correct phys offset in ee_ram_stub).

Stack-top is approximated as (ee_ram_bytes - 0x10) since real PS2
ELFs don't carry a stack pointer in their headers; the TB will set
$sp to this if the manifest is read.

Usage:
    elf_to_eeram.py --in path/to/game.elf --out-prefix /tmp/game

Verdict-aware notes:
  * Segments overflowing the EE RAM image cause a fatal error.
  * Segments overlapping each other are flagged but not fatal — the
    later one wins (matches how a real loader would behave).
"""

import sys
import struct
import argparse


ELFMAG    = b"\x7fELF"
ELFCLASS32 = 1
ELFDATA2LSB = 1
EM_MIPS   = 8
ET_EXEC   = 2
ET_DYN    = 3
PT_LOAD   = 1


def parse_elf32_le(data: bytes):
    """Return (entry, [(p_vaddr, p_offset, p_filesz, p_memsz), ...])
    for PT_LOAD segments of a 32-bit little-endian MIPS ELF.
    Raises ValueError on bad magic / wrong class / wrong arch.
    """
    if len(data) < 52 or data[:4] != ELFMAG:
        raise ValueError("not an ELF file (bad magic)")
    if data[4] != ELFCLASS32:
        raise ValueError(f"only ELFCLASS32 supported (got class={data[4]})")
    if data[5] != ELFDATA2LSB:
        raise ValueError(f"only little-endian supported (got data={data[5]})")

    (e_type, e_machine, e_version, e_entry, e_phoff, e_shoff, e_flags,
     e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx) = \
        struct.unpack_from("<HHIIIIIHHHHHH", data, 16)

    if e_machine != EM_MIPS:
        raise ValueError(f"only EM_MIPS supported (got machine={e_machine})")
    if e_type not in (ET_EXEC, ET_DYN):
        raise ValueError(f"only ET_EXEC / ET_DYN supported (got type={e_type})")

    pt_load = []
    for i in range(e_phnum):
        off = e_phoff + i * e_phentsize
        (p_type, p_offset, p_vaddr, p_paddr, p_filesz, p_memsz,
         p_flags, p_align) = struct.unpack_from("<IIIIIIII", data, off)
        if p_type == PT_LOAD:
            pt_load.append((p_vaddr, p_offset, p_filesz, p_memsz))

    return e_entry, pt_load


def build_image(elf_bytes: bytes, ee_ram_bytes: int):
    entry, segs = parse_elf32_le(elf_bytes)
    image = bytearray(ee_ram_bytes)

    placed = []
    for (p_vaddr, p_offset, p_filesz, p_memsz) in segs:
        phys = p_vaddr & 0x1FFFFFFF  # strip kseg/kuseg bits
        if phys + p_memsz > ee_ram_bytes:
            raise ValueError(
                f"PT_LOAD at vaddr=0x{p_vaddr:08x} phys=0x{phys:08x} "
                f"size=0x{p_memsz:x} overflows EE RAM (0x{ee_ram_bytes:x})")
        # Detect overlap (informational only).
        for (lo, hi) in placed:
            if not (phys + p_memsz <= lo or phys >= hi):
                print(f"[elf_to_eeram] WARNING: PT_LOAD at phys=0x{phys:08x} "
                      f"size=0x{p_memsz:x} overlaps prior placement",
                      file=sys.stderr)
        placed.append((phys, phys + p_memsz))

        # Copy p_filesz bytes from file at p_offset → phys. p_memsz can
        # be larger than p_filesz (.bss tail); image is already zero-
        # initialised so the tail is naturally zero.
        chunk = elf_bytes[p_offset:p_offset + p_filesz]
        image[phys:phys + p_filesz] = chunk
        print(f"[elf_to_eeram] placed PT_LOAD vaddr=0x{p_vaddr:08x} "
              f"phys=0x{phys:08x} filesz=0x{p_filesz:x} memsz=0x{p_memsz:x}")

    return entry, image


def qword_to_hex(image: bytearray, qw_phys: int) -> str:
    """MSB-first hex string for the qword at byte offset qw_phys."""
    bytes16 = image[qw_phys:qw_phys + 16]
    return bytes16[::-1].hex()


def emit_image_hex(image: bytearray, path: str) -> None:
    qw_size = 16
    with open(path, "w") as f:
        f.write("// Ch270 ELF-derived 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:
            f.write("@00000000\n00000000000000000000000000000000\n")


def emit_manifest_hex(path: str, entry: int, stack_top: int) -> None:
    with open(path, "w") as f:
        f.write("// Ch270 manifest from ELF\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 main() -> int:
    p = argparse.ArgumentParser(description=__doc__,
                                formatter_class=argparse.RawDescriptionHelpFormatter)
    p.add_argument("--in", dest="elf_in", required=True,
                   help="input ELF path")
    p.add_argument("--out-prefix", required=True,
                   help="output file prefix")
    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)")
    args = p.parse_args()

    with open(args.elf_in, "rb") as f:
        elf_bytes = f.read()

    entry, image = build_image(elf_bytes, args.ee_ram_bytes)
    stack_top = args.ee_ram_bytes - 0x10
    emit_image_hex(image, f"{args.out_prefix}.image.hex")
    emit_manifest_hex(f"{args.out_prefix}.manifest.hex", entry, stack_top)
    print(f"[elf_to_eeram] wrote {args.out_prefix}.image.hex + "
          f"{args.out_prefix}.manifest.hex (entry=0x{entry:08x}, "
          f"stack_top=0x{stack_top:08x}, ee_ram={args.ee_ram_bytes} bytes)")
    return 0


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