#!/usr/bin/env python3
"""Compare two traces in the retroDE_ps2 common envelope.

Implements the diff semantics specified in sim/golden/trace_compare_spec.md:

  - parse both files, skipping blank lines and comments starting with '#',
  - enforce exactly 8 columns per non-comment line,
  - validate cycle monotonicity within each input,
  - filter to (subsystem, event) pair (default: EE IFETCH),
  - require non-empty filtered sets on both sides,
  - require equal filtered lengths,
  - compare by record order, not by cycle,
  - the fields that must match exactly: subsystem, event, arg0..arg3, flags,
  - cycle is informational only.

Exit codes:
  0  pass
  1  semantic mismatch (length, field, or empty filtered set)
  2  usage or missing-file error
  3  malformed input / parse failure
"""
import argparse
import sys
from dataclasses import dataclass

EXIT_PASS = 0
EXIT_MISMATCH = 1
EXIT_USAGE = 2
EXIT_MALFORMED = 3


@dataclass
class Record:
    line_no: int
    cycle: int
    subsystem: str
    event: str
    arg0: int
    arg1: int
    arg2: int
    arg3: int
    flags: int


def _parse_int(s: str) -> int:
    s = s.strip()
    if s.startswith(("0x", "0X")):
        return int(s, 16)
    return int(s, 10)


def _parse_flags(s: str) -> int:
    s = s.strip()
    if s == "-":
        return 0
    if s.startswith(("0x", "0X")):
        return int(s, 16)
    raise ValueError(f"unrecognized flags token: {s!r}")


def _fail_malformed(path: str, line_no: int, reason: str) -> None:
    print(f"ERROR: {path}:{line_no}: {reason}", file=sys.stderr)
    sys.exit(EXIT_MALFORMED)


def parse_trace(path: str) -> list[Record]:
    try:
        f = open(path, "r")
    except OSError as e:
        print(f"ERROR: cannot open {path}: {e}", file=sys.stderr)
        sys.exit(EXIT_USAGE)

    records: list[Record] = []
    with f:
        for line_no, raw in enumerate(f, 1):
            stripped = raw.strip()
            if not stripped or stripped.startswith("#"):
                continue
            parts = stripped.split()
            if len(parts) != 8:
                _fail_malformed(path, line_no,
                                f"expected 8 columns, got {len(parts)}")
            try:
                cycle = _parse_int(parts[0])
                arg0 = _parse_int(parts[3])
                arg1 = _parse_int(parts[4])
                arg2 = _parse_int(parts[5])
                arg3 = _parse_int(parts[6])
                flags = _parse_flags(parts[7])
            except ValueError as e:
                _fail_malformed(path, line_no, str(e))
            records.append(Record(
                line_no=line_no, cycle=cycle,
                subsystem=parts[1], event=parts[2],
                arg0=arg0, arg1=arg1, arg2=arg2, arg3=arg3, flags=flags,
            ))

    # Monotonic cycle check on the raw (unfiltered) stream.
    for i in range(1, len(records)):
        if records[i].cycle < records[i - 1].cycle:
            _fail_malformed(
                path, records[i].line_no,
                f"non-monotonic cycle: {records[i].cycle} "
                f"< previous {records[i - 1].cycle}"
            )

    return records


def _filter(recs: list[Record], subsystem: str, event: str) -> list[Record]:
    return [r for r in recs if r.subsystem == subsystem and r.event == event]


def _fmt_record(r: Record, subsystem: str, event: str) -> str:
    return (f"{subsystem} {event} "
            f"arg0=0x{r.arg0:016x} arg1=0x{r.arg1:016x} "
            f"arg2=0x{r.arg2:016x} arg3=0x{r.arg3:016x} "
            f"flags=0x{r.flags:08x}")


def main() -> int:
    ap = argparse.ArgumentParser(description="retroDE_ps2 trace comparator")
    ap.add_argument("rtl", help="RTL trace file")
    ap.add_argument("golden", help="golden trace file")
    ap.add_argument("--subsystem", default="EE",
                    help="filter subsystem (default: EE)")
    ap.add_argument("--event", default="IFETCH",
                    help="filter event (default: IFETCH)")
    args = ap.parse_args()

    rtl_all = parse_trace(args.rtl)
    golden_all = parse_trace(args.golden)

    rtl = _filter(rtl_all, args.subsystem, args.event)
    golden = _filter(golden_all, args.subsystem, args.event)

    if not rtl:
        print(f"FAIL: {args.rtl}: no {args.subsystem}/{args.event} records "
              f"after filter", file=sys.stderr)
        return EXIT_MISMATCH
    if not golden:
        print(f"FAIL: {args.golden}: no {args.subsystem}/{args.event} records "
              f"after filter", file=sys.stderr)
        return EXIT_MISMATCH

    if len(rtl) != len(golden):
        longer = "rtl" if len(rtl) > len(golden) else "golden"
        first_missing = min(len(rtl), len(golden))
        print(f"FAIL: length mismatch — rtl={len(rtl)} golden={len(golden)}",
              file=sys.stderr)
        print(f"  {longer} has more records; "
              f"first missing filtered index = {first_missing}",
              file=sys.stderr)
        return EXIT_MISMATCH

    must_match = ("subsystem", "event", "arg0", "arg1", "arg2", "arg3", "flags")
    for i, (r, g) in enumerate(zip(rtl, golden)):
        diffs = [f for f in must_match if getattr(r, f) != getattr(g, f)]
        if diffs:
            print(f"FAIL: mismatch at filtered record {i}", file=sys.stderr)
            print(f"  rtl:    {_fmt_record(r, args.subsystem, args.event)}",
                  file=sys.stderr)
            print(f"  golden: {_fmt_record(g, args.subsystem, args.event)}",
                  file=sys.stderr)
            print(f"  diff:   {', '.join(diffs)}", file=sys.stderr)
            return EXIT_MISMATCH

    print(f"PASS: matched {len(rtl)} {args.subsystem}/{args.event} records "
          f"(cycle ignored, order-based compare)")
    return EXIT_PASS


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