#!/usr/bin/env python3
"""Render a cross-port UBER perf table in Markdown.

Reads each port's joeylog.txt (the file UBER appends ops/sec lines to)
and emits a Markdown table comparing all ports against a reference.
The reference column shows absolute ops/sec; every other port shows a
ratio of (port / reference) so a glance tells you whether each target
is at, above, or below the reference.

Usage:
    tools/uber-perf-table [--ref iigs] [--out PATH] [--repo PATH]

Defaults pull the 4 standard log paths under the repo's build/ tree;
pass --repo if running from elsewhere. The reference port (default
IIgs, which DESIGN.md calls the perf floor) defines the ratio
denominator. Ports whose log file is missing are listed in the table
header as "(no log)" and their columns are filled with "-".

Example, run from the repo root:

    tools/uber-perf-table > PERF.md

To compare against a different reference (e.g. profile DOS as the
floor for a DOS-only project):

    tools/uber-perf-table --ref dos
"""

import argparse
import os
import re
import sys

LINE_RE = re.compile(
    r"UBER:\s+(?P<op>[^:]+):\s+\d+\s+iters\s+/\s+\d+\s+frames\s+=\s+(?P<ops>\d+)\s+ops/sec"
)

# Per-port log path relative to the repo root, in the column order the
# table renders. Reference port appears first.
PORTS = [
    ("iigs",    "IIgs",     "build/iigs/bin/joeylog.txt"),
    ("amiga",   "Amiga",    "build/amiga/bin/joeylog.txt"),
    ("atarist", "Atari ST", "build/atarist/bin/joeylog.txt"),
    ("dos",     "DOS",      "build/dos/bin/JOEYLOG.TXT"),
]


# Appended below the table when IIgs is the reference port. The IIgs is
# a 2.8 MHz 65816 moving a 32000-byte SHR screen, so any full-screen
# byte-move op has a hard throughput ceiling; numbers above it are a
# measurement artifact, not real speed.
IIGS_CEILING_NOTE = (
    "\n"
    "> **IIgs full-screen ceiling (~29 ops/sec).** A full-screen op moves\n"
    "> 32000 bytes = 16000 word-stores x 6 cyc (STA long,X / PEI, both\n"
    "> 3 cyc/byte) / 2.8 MHz ~= 34 ms ~= 29 ops/sec maximum. So\n"
    "> `jlSurfaceClear`, `jlFillRect 320x200`, and `jlStagePresent full`\n"
    "> cannot legitimately exceed ~29; a higher reading means the benchmark\n"
    "> clock was perturbed. The clock is `jlFrameCount` = the VBL-interrupt-\n"
    "> driven GetTick, so an op that holds `SEI` while it runs can over-\n"
    "> report its ops/sec. Treat full-screen IIgs numbers above ~29 as\n"
    "> inflated and cross-check against the cycle model.\n"
)


def parse_log(path):
    """Return {op: ops/sec} from a UBER log. Last entry per op wins."""
    perf = {}
    with open(path) as f:
        for line in f:
            m = LINE_RE.search(line)
            if m:
                perf[m.group("op").strip()] = int(m.group("ops"))
    return perf


def format_cell(port_ops, ref_ops):
    """Non-reference cell: absolute ops/sec plus percentage of the
    reference (the IIgs floor), e.g. "6703 (464%)". When the reference
    value for the op is missing (IIgs column pending), the absolute
    number still prints -- data is never dropped just because the floor
    is unavailable. Sub-100% cells are bolded: the reference port is
    the perf floor, so below-floor cells are the ones worth attention.
    """
    if port_ops is None:
        return "-"
    if ref_ops is None or ref_ops == 0:
        return str(port_ops)
    pct = port_ops / ref_ops * 100.0
    cell = f"{port_ops} ({pct:.0f}%)"
    if pct < 100.0:
        cell = f"**{cell}**"
    return cell


def render_table(port_data, ops_in_order, ref_key):
    headers = ["Op"]
    for key, label, _ in PORTS:
        if not port_data[key]["loaded"]:
            label = f"{label} (no log)"
        if key == ref_key:
            headers.append(f"{label} (ops/sec)")
        else:
            headers.append(f"{label} (ops/sec, % of {ref_key.upper()})")

    rows = [headers, ["---"] * len(headers)]
    for op in ops_in_order:
        row = [op]
        ref_ops = port_data[ref_key]["perf"].get(op)
        for key, _, _ in PORTS:
            ops = port_data[key]["perf"].get(op)
            if key == ref_key:
                row.append(str(ops) if ops is not None else "pending")
            else:
                row.append(format_cell(ops, ref_ops))
        rows.append(row)

    return "\n".join("| " + " | ".join(r) + " |" for r in rows)


def main(argv):
    parser = argparse.ArgumentParser(
        description="Render a cross-port UBER perf table in Markdown."
    )
    parser.add_argument("--ref", default="iigs",
                        choices=[p[0] for p in PORTS],
                        help="Reference port (default: iigs)")
    parser.add_argument("--out", default=None,
                        help="Write table to this file instead of stdout")
    parser.add_argument("--repo", default=None,
                        help="Repo root (default: parent of this script)")
    args = parser.parse_args(argv[1:])

    if args.repo is None:
        args.repo = os.path.dirname(os.path.dirname(os.path.abspath(argv[0])))

    port_data = {}
    union_ops = []
    seen = set()
    for key, _, rel in PORTS:
        path = os.path.join(args.repo, rel)
        try:
            perf = parse_log(path)
            loaded = True
        except OSError:
            perf = {}
            loaded = False
        port_data[key] = {"perf": perf, "loaded": loaded, "path": path}
        for op in perf:
            if op not in seen:
                seen.add(op)
                union_ops.append(op)

    if not port_data[args.ref]["loaded"]:
        sys.stderr.write(
            f"error: reference log missing: {port_data[args.ref]['path']}\n"
            f"       run UBER on {args.ref} first, then retry.\n"
        )
        return 2

    if not union_ops:
        sys.stderr.write("error: no UBER ops parsed from any log\n")
        return 2

    # Use the reference port's op order so the table matches UBER's
    # natural sequence (surfaceClear first, joeyAudioIsPlayingMod last).
    ref_ops = list(port_data[args.ref]["perf"].keys())
    extras = [op for op in union_ops if op not in port_data[args.ref]["perf"]]
    ordered = ref_ops + extras

    table = render_table(port_data, ordered, args.ref)
    if args.ref == "iigs":
        table += "\n" + IIGS_CEILING_NOTE
    if args.out:
        with open(args.out, "w") as f:
            f.write(table + "\n")
    else:
        sys.stdout.write(table + "\n")
    return 0


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