#!/usr/bin/env python3
"""Python adapter: extracts the REAL grammar functions out of pico-cp/app.py (no copy) and
emits the neutral normalized structure from docs/track-format.md §5.

app.py can't be imported directly (it does hardware init at module load), so we pull just the
pure codec nodes by name via `ast` and exec them in isolation. This tests the actual firmware
code and survives edits as long as the function/constant names persist.
"""
import ast
import json
import os
import sys

APP = os.path.join(os.path.dirname(__file__), "..", "..", "pico-cp", "app.py")
WANT = {"PAT", "ORN", "PRIO", "PAT_CH", "ORN_CH", "_cell_ch", "SOUND_GM", "GM_NUM", "_euclid", "parse_program", "_parse_lane", "lane_to_str"}

with open(APP) as f:
    _src = f.read()

_segs = []
for node in ast.parse(_src).body:
    name = None
    if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
        name = node.targets[0].id
    elif isinstance(node, ast.FunctionDef):
        name = node.name
    if name in WANT:
        _segs.append(ast.get_source_segment(_src, node))

NS = {}
exec("\n".join(_segs), NS)


def _prog_str(bpm, lanes, bars, ramp, trainer, rep=None, end=None):
    # Mirrors app.py App._prog_str using the real lane_to_str.
    parts = ["t" + str(bpm)]
    if bars:
        parts.append("b" + str(bars))
    if ramp:
        parts.append("rmp%d/%d/%d" % (ramp.get("start", bpm), ramp["amt"], ramp["every"]))
    if trainer:
        parts.append("tr%d/%d" % (trainer["play"], trainer["mute"]))
    for L in lanes:
        parts.append(NS["lane_to_str"](L))
    if end is not None:
        if rep and rep > 1:
            parts.append("rep=" + str(rep))
        parts.append("end=" + ("stop" if end == "stop" else "next" if end == 1 else ("+%d" % end if end > 0 else str(end))))
    return ";".join(parts)


def _gain_db(g):
    if not g:
        return 0
    try:
        return float(str(g).lstrip("@"))
    except ValueError:
        return 0


def normalize(patch):
    bpm, lanes, bars, ramp, trainer, rep, end = NS["parse_program"](patch)
    return {
        "bpm": bpm,
        "bars": bars,
        "volume": None,   # device has no master-volume token
        "countMs": 0,     # device has no count-in
        "ramp": {"start": ramp.get("start", bpm), "amt": ramp["amt"], "every": ramp["every"]} if ramp else None,
        "trainer": {"play": trainer["play"], "mute": trainer["mute"]} if trainer else None,
        "rep": None if end is None else (rep if rep else 1),   # rep only meaningful with end; defaults to 1
        "end": end,
        "lanes": [
            {
                "sound": L["sound"],
                "groups": list(L.get("groups", [4])),
                "sub": L["sub"],
                "swing": bool(L["swing"]),
                "poly": bool(L["poly"]),
                "mute": bool(L["mute"]),
                "gainDb": _gain_db(L.get("gain", "")),
                "levels": [int(v) for v in L["levels"]],
                "orns": [int(v) for v in L.get("orns", [])],
            }
            for L in lanes
        ],
    }


def canonical(patch):
    return _prog_str(*NS["parse_program"](patch))


if __name__ == "__main__":
    patch = sys.argv[1] if len(sys.argv) > 1 else ""
    # Re-parse the canonical form too, so the runner can check idempotency in one call.
    c1 = canonical(patch)
    c2 = canonical(c1)
    sys.stdout.write(json.dumps({"norm": normalize(patch), "canonical": c1, "idempotent": c1 == c2}))
