#!/usr/bin/env python3
"""
retroDE_ps2 — Quartus report parser for the DE25-Nano
top_psmct32_raster_demo board top.

Summarizes the most recent quartus_compile run by pulling:
  - Resource usage (LE / ALM / register / BRAM / DSP / IO) from
    the fit summary
  - Warning class counts from the syn / fit / sta logs
  - Unconstrained / failing-path summaries from the STA report
  - Highlighting scaffold/stub components on the build path
    ($readmemh image loads, BRAM-inferred RAM identifiers, PLL
    sim-stub usage, VIRTUAL_PIN-marked outputs) so a future
    PHY/PLL chapter can compare deltas

Usage (from any directory):
  parse_reports.py <output_files dir>
"""

import argparse
import re
import sys
from pathlib import Path


def find_first(out_dir: Path, *patterns: str) -> Path | None:
    """Return the first file matching any of the glob patterns."""
    for pat in patterns:
        for p in sorted(out_dir.glob(pat)):
            if p.is_file():
                return p
    return None


def section(title: str) -> None:
    print()
    print(f"== {title} ==")


def parse_fit_summary(out_dir: Path) -> None:
    """Quartus Pro stores fit summary in <project>.fit.summary."""
    f = find_first(out_dir, "*.fit.summary", "*.fit.rpt")
    if f is None:
        section("Fit summary")
        print("  (no .fit.summary or .fit.rpt found — fit step did not complete)")
        return
    section(f"Fit summary  ({f.name})")
    txt = f.read_text(errors="ignore")
    # Pull the leading "<key> : <value>" lines until first blank.
    for line in txt.splitlines()[:40]:
        if not line.strip():
            continue
        if line.startswith("---") or line.startswith("==="):
            continue
        print(f"  {line.strip()}")


def parse_resource_usage(out_dir: Path) -> None:
    """Pull the resource-usage table from the fit report."""
    f = find_first(out_dir, "*.fit.rpt")
    if f is None:
        return
    section(f"Resource usage  (from {f.name})")
    txt = f.read_text(errors="ignore")
    # Look for "Logic utilization" / "Total ALMs needed" / "Total
    # registers" / "Total block memory bits" — Pro and Standard
    # use slightly different names; grab anything matching.
    keys = [
        "Logic utilization",
        "Total ALMs needed",
        "Total ALMs",
        "ALMs needed [=A-B+C]",
        "ALMs used in final placement",
        "Total registers",
        "Total combinational ALUTs",
        "Total pins",
        "Total virtual pins",
        "Total block memory bits",
        "Total block memory implementation bits",
        "Total RAM block",
        "Total DSP Blocks",
    ]
    found = False
    for line in txt.splitlines():
        stripped = line.strip()
        for k in keys:
            if stripped.startswith("; " + k) or stripped.startswith(k):
                print(f"  {stripped.lstrip(';').strip()}")
                found = True
                break
    if not found:
        print("  (no resource-usage rows found in the fit report)")


def parse_log_warnings(out_dir: Path) -> None:
    """Walk the per-step build_logs and count Quartus warning IDs."""
    log_dir = out_dir / "build_logs"
    if not log_dir.is_dir():
        section("Warnings (per-step)")
        print(f"  (no build_logs dir at {log_dir})")
        return
    for log in sorted(log_dir.glob("*.log")):
        section(f"Log: {log.name}")
        txt = log.read_text(errors="ignore")
        warn_re = re.compile(r"Warning \((\d+)\):\s*(.+)")
        info_re = re.compile(r"Critical Warning \((\d+)\):\s*(.+)")
        err_re = re.compile(r"Error \((\d+)\):\s*(.+)")
        warns: dict[str, int] = {}
        crit: dict[str, int] = {}
        errs: dict[str, int] = {}
        for line in txt.splitlines():
            for rx, bucket in ((info_re, crit), (warn_re, warns), (err_re, errs)):
                m = rx.match(line.strip())
                if m:
                    bucket[m.group(1)] = bucket.get(m.group(1), 0) + 1
                    break
        # Trailing summary line, if present, e.g. "...successful. 0 errors, N warnings"
        summary = ""
        for line in txt.splitlines()[-50:]:
            if "errors" in line and "warning" in line.lower():
                summary = line.strip()
        if summary:
            print(f"  {summary}")
        if errs:
            print(f"  errors by ID:           {dict(sorted(errs.items()))}")
        if crit:
            print(f"  critical warnings by ID: {dict(sorted(crit.items()))}")
        if warns:
            top = sorted(warns.items(), key=lambda kv: -kv[1])[:5]
            print(f"  top warnings by ID:     {dict(top)}")
        if not (errs or crit or warns):
            print("  (no Quartus error / warning / critical-warning lines parsed)")


def parse_sta_summary(out_dir: Path) -> None:
    """Pull unconstrained-paths and slack summaries from the STA report."""
    f = find_first(out_dir, "*.sta.rpt", "*.sta.summary")
    if f is None:
        section("STA summary")
        print("  (no .sta.rpt found — sta step did not complete)")
        return
    section(f"STA summary  ({f.name})")
    txt = f.read_text(errors="ignore")
    # Look for "Worst-case slack" / "Unconstrained paths" / fmax tables.
    interesting = [
        "Worst-case slack",
        "Setup Slack",
        "Hold Slack",
        "Unconstrained paths",
        "Unconstrained Paths",
        "fmax",
        "Fmax",
        "Slow",
    ]
    printed = 0
    for line in txt.splitlines():
        for k in interesting:
            if k in line:
                print(f"  {line.strip()}")
                printed += 1
                if printed >= 30:
                    break
        if printed >= 30:
            break
    if printed == 0:
        print("  (no recognized STA summary lines)")


def parse_ch151_stubs(out_dir: Path) -> None:
    """Highlight scaffold/stub components on the build path
    ($readmemh image loads, BRAM-inferred RAM identifiers, PLL
    sim-stub usage, VIRTUAL_PIN-marked outputs) so future PHY/PLL
    chapters can compare baseline deltas."""
    log = out_dir / "build_logs" / "quartus_syn.log"
    if not log.is_file():
        section("Scaffold-path footprint")
        print("  (no syn log)")
        return
    section("Scaffold-path footprint  (from quartus_syn.log)")
    txt = log.read_text(errors="ignore")
    markers = [
        ("$readmemh image loaded",       r"loading image: (.+\.mem)"),
        ("RAM extracted (BRAM infer)",   r"extracting RAM for identifier '([^']+)'"),
        ("PLL sim-stub (de25_nano_pll)", r"de25_nano_pll_stub"),
        ("VIRTUAL_PIN ON",               r"VIRTUAL_PIN .*VIDEO_"),
    ]
    for label, pat in markers:
        rx = re.compile(pat)
        hits = sorted(set(rx.findall(txt)))
        if hits:
            print(f"  {label}:")
            for h in hits[:8]:
                print(f"    {h}")
            if len(hits) > 8:
                print(f"    ... (+{len(hits) - 8} more)")
        else:
            print(f"  {label}:  (none)")


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("out_dir", help="path to output_files dir")
    args = parser.parse_args()
    out_dir = Path(args.out_dir).resolve()
    if not out_dir.is_dir():
        print(f"error: {out_dir} is not a directory", file=sys.stderr)
        return 1

    print(f"=== Quartus build report  ({out_dir}) ===")
    parse_fit_summary(out_dir)
    parse_resource_usage(out_dir)
    parse_sta_summary(out_dir)
    parse_log_warnings(out_dir)
    parse_ch151_stubs(out_dir)
    print()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
