#!/usr/bin/env -S uv run --script
"""Trace Lost Pig V8 opcodes — find what's needed for V5+ support.

Runs the interpreter step-by-step, collecting unique opcodes hit.
When an unimplemented opcode is encountered, reports what was seen
and what's missing.
"""

# ruff: noqa: E402
import sys
from collections import Counter
from pathlib import Path

project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root / "src"))

from mudlib.zmachine import ZMachine, zopdecoder, zstream, zui
from mudlib.zmachine.trivialzui import (
    TrivialAudio,
    TrivialFilesystem,
    TrivialScreen,
)
from mudlib.zmachine.zcpu import (
    ZCpuNotImplemented,
    ZCpuQuit,
    ZCpuRestart,
)

story_path = project_root / "content" / "stories" / "LostPig.z8"
if not story_path.exists():
    print(f"ERROR: {story_path} not found")
    sys.exit(1)

story_bytes = story_path.read_bytes()
print(f"Loaded LostPig.z8: {len(story_bytes)} bytes, version {story_bytes[0]}")


class AutoInputStream(zstream.ZInputStream):
    """Input stream that auto-feeds commands."""

    def __init__(self):
        super().__init__()
        self._input_count = 0
        self._max_inputs = 3

    def read_line(self, *args, **kwargs):
        self._input_count += 1
        if self._input_count > self._max_inputs:
            raise ZCpuQuit
        return "look"


audio = TrivialAudio()
screen = TrivialScreen()
keyboard = AutoInputStream()
filesystem = TrivialFilesystem()
ui = zui.ZUI(audio, screen, keyboard, filesystem)
zm = ZMachine(story_bytes, ui)

opcodes_seen = Counter()
step_count = 0
max_steps = 500_000

print(f"Tracing up to {max_steps} steps...")
print()

try:
    while step_count < max_steps:
        pc = zm._cpu._opdecoder.program_counter
        (opcode_class, opcode_number, operands) = (
            zm._cpu._opdecoder.get_next_instruction()
        )

        cls_str = zopdecoder.OPCODE_STRINGS.get(opcode_class, f"?{opcode_class}")
        key = f"{cls_str}:{opcode_number:02x}"

        try:
            implemented, func = zm._cpu._get_handler(opcode_class, opcode_number)
        except Exception as e:
            print(f"ILLEGAL at step {step_count}: {key} (pc={pc:#x})")
            print(f"  {e}")
            break

        opcodes_seen[f"{key} ({func.__name__})"] += 1

        if not implemented:
            print(f"UNIMPLEMENTED at step {step_count}: {key} -> {func.__name__}")
            print(f"  PC: {pc:#x}")
            print()
            break

        try:
            func(zm._cpu, *operands)
        except ZCpuQuit:
            print(f"Game quit after {step_count} steps")
            break
        except ZCpuRestart:
            print(f"Game restart after {step_count} steps")
            break
        except ZCpuNotImplemented as e:
            print(f"NOT IMPLEMENTED at step {step_count}: {key} -> {func.__name__}")
            print(f"  PC: {pc:#x}")
            print(f"  Error: {e}")
            print()
            break
        except Exception as e:
            print(f"ERROR at step {step_count}: {key} -> {func.__name__}")
            print(f"  PC: {pc:#x}")
            print(f"  Operands: {operands}")
            print(f"  {type(e).__name__}: {e}")
            import traceback

            traceback.print_exc()
            zm._cpu._dump_trace()
            break

        step_count += 1

except KeyboardInterrupt:
    print(f"\nInterrupted at step {step_count}")

print(f"\nTotal steps: {step_count}")
print(f"Unique opcodes seen: {len(opcodes_seen)}")
print()
print("Opcodes by frequency:")
for op, count in opcodes_seen.most_common():
    print(f"  {count:>8}  {op}")
