#!/usr/bin/env python3
"""Decode a VPX_FIFODUMP capture (Phase 3a).

The dump is a sequence of records, each one FIFO burst (the bytes REP OUTSWed
to the FIFO port between two outputData tag writes):

    'VPXM' u32-LE length, then <length> raw bytes

Wire model (VR_COMMS.C velocirender_transmit, FIFO path): each transmit is two
bursts -- burst 1 is the 4-byte action word, burst 2 the message data. So
records pair up into [action][data] messages. velocirender_packetize splits
payloads >508 bytes into multiple transmits with the same action.

Usage: decode_fifodump.py <dump> [--hex N] [--action A]
"""
import struct
import sys
from collections import Counter

# DPL3 VR_PROT.H enum -- the Rel4.10 build extends this (0x2D sync etc.);
# names beyond the DPL3 enum are provisional and marked with '?'.
ACTION_NAMES = {
    0: "init", 1: "create", 2: "delete", 3: "flush",
    4: "sect_pixel", 5: "sect_vector",
    6: "dcs_nest", 7: "dcs_link", 8: "dcs_prune",
    9: "draw_scene", 10: "draw_scene_complete",
    11: "list_add", 12: "list_remove", 13: "morph",
    14: "version", 15: "statistics", 16: "readpixels",
    17: "hspcode", 18: "860code", 19: "860data", 20: "860bss", 21: "860args",
    22: "set_geom_verts", 23: "set_texmap_texels",
    0x2D: "sync(rel410)",
}


def read_records(path):
    recs = []
    with open(path, "rb") as f:
        while True:
            hdr = f.read(8)
            if len(hdr) < 8:
                break
            magic, ln = hdr[:4], struct.unpack("<I", hdr[4:])[0]
            if magic != b"VPXM":
                raise SystemExit(f"bad magic at record {len(recs)}: {magic!r}")
            recs.append(f.read(ln))
    return recs


def pair_messages(recs):
    """Split burst records into (action, data) messages.

    DPL3's transmit sends two bursts (action, then data); the Rel4.10 build
    observed in captures pumps [action:4][data] as ONE burst per tag write.
    Handle both: a 4-byte record whose next record isn't 4 bytes pairs with
    it; otherwise the record's first word is the action."""
    msgs = []
    i = 0
    while i < len(recs):
        r = recs[i]
        if len(r) == 4 and i + 1 < len(recs) and len(recs[i + 1]) > 4:
            msgs.append((struct.unpack("<I", r)[0], recs[i + 1]))
            i += 2
            continue
        if len(r) >= 4:
            msgs.append((struct.unpack("<I", r[:4])[0], r[4:]))
        else:
            msgs.append((None, r))
        i += 1
    return msgs


def aname(a):
    if a is None:
        return "<no-action>"
    n = ACTION_NAMES.get(a)
    return f"{a}:{n}" if n else f"{a}(0x{a:X})?"


def hexdump(b, limit=64):
    out = []
    for i in range(0, min(len(b), limit), 16):
        row = b[i:i + 16]
        hx = " ".join(f"{c:02x}" for c in row)
        tx = "".join(chr(c) if 32 <= c < 127 else "." for c in row)
        out.append(f"    {i:04x}  {hx:<48}  {tx}")
    if len(b) > limit:
        out.append(f"    ... {len(b)} bytes total")
    return "\n".join(out)


def floats(b, n=8):
    k = min(n, len(b) // 4)
    return " ".join(f"{v:.4g}" for v in struct.unpack(f"<{k}f", b[:k * 4]))


def ints(b, n=8):
    k = min(n, len(b) // 4)
    return " ".join(f"{v:#x}" for v in struct.unpack(f"<{k}I", b[:k * 4]))


def main():
    path = sys.argv[1]
    hexn = 0
    only = None
    if "--hex" in sys.argv:
        hexn = int(sys.argv[sys.argv.index("--hex") + 1])
    if "--action" in sys.argv:
        only = int(sys.argv[sys.argv.index("--action") + 1], 0)

    recs = read_records(path)
    msgs = pair_messages(recs)
    print(f"{len(recs)} burst records -> {len(msgs)} messages\n")

    counts = Counter()
    sizes = Counter()
    for a, d in msgs:
        counts[a] += 1
        sizes[a] += len(d)

    print("action                     count   total-bytes  sizes")
    persize = {}
    for a, d in msgs:
        persize.setdefault(a, Counter())[len(d)] += 1
    for a in sorted(counts, key=lambda x: (x is None, x)):
        sz = ",".join(f"{s}x{c}" if c > 1 else f"{s}"
                      for s, c in sorted(persize[a].items())[:6])
        print(f"{aname(a):<26} {counts[a]:>5}   {sizes[a]:>11}  {sz}")

    if hexn or only is not None:
        print()
        shown = 0
        for i, (a, d) in enumerate(msgs):
            if only is not None and a != only:
                continue
            print(f"[{i}] {aname(a)} len={len(d)}")
            if d:
                print("  ints:  ", ints(d))
                print("  floats:", floats(d))
                print(hexdump(d, hexn if hexn else 64))
            shown += 1
            if hexn and shown >= 40 and only is None:
                print("... (truncated)")
                break


if __name__ == "__main__":
    main()
