#!/usr/bin/env python3
"""J19 pair scan — drive one GPIO at a time; listen on the rest for coupling.

Cycles BCM 11/9/10/8/2/3 (FPC pins 1–6). Pins 7–8 have no GPIO on this
carrier. For each drive line: set others to input (bias off), square-wave the
driver, count listen-pin edges that line up with drive edges.

  sudo python3 hs_j19_pair_scan.py
  sudo python3 hs_j19_pair_scan.py --dwell-s 1.5 --period-ms 20

Prints a pairing matrix when done. Restores all lines to input pull-up.
"""

from __future__ import annotations

import argparse
import signal
import sys
import time

from j19_io_mapper import MapperError, pinctrl, pinctrl_get, set_input_pull_up, set_output_level

# FPC pin, BCM, label (pins 7–8 nc)
J19_LINES: list[tuple[int, int | None, str]] = [
    (1, 11, "M-S_OUT"),
    (2, 9, "M-S_IN"),
    (3, 10, "OIS_OUT"),
    (4, 8, "OIS_IN"),
    (5, 2, "LED1"),
    (6, 3, "LED2"),
    (7, None, "nc"),
    (8, None, "nc"),
]

GPIO_BCMS = [bcm for _, bcm, _ in J19_LINES if bcm is not None]


def _set_input_no_bias(bcm: int) -> None:
    """Input, bias disabled — board has no pull-downs; avoid Pi pull-up masking."""
    cp = pinctrl("set", str(bcm), "ip", "pn")
    if cp.returncode != 0:
        # Fallback if pn unsupported
        cp = pinctrl("set", str(bcm), "ip")
        if cp.returncode != 0:
            raise MapperError(
                f"pinctrl set {bcm} ip failed: {(cp.stderr or cp.stdout).strip()}"
            )


def _level_hi(bcm: int) -> bool | None:
    text = pinctrl_get(bcm).lower()
    if "| hi" in text or text.rstrip().endswith("hi"):
        return True
    if "| lo" in text or text.rstrip().endswith("lo"):
        return False
    return None


def main() -> int:
    ap = argparse.ArgumentParser(description="J19 one-at-a-time drive / listen pair scan")
    ap.add_argument("--period-ms", type=float, default=20.0, help="drive half-period ms")
    ap.add_argument("--dwell-s", type=float, default=1.2, help="seconds per drive pin")
    ap.add_argument(
        "--min-edges",
        type=int,
        default=8,
        help="min listen edges to call a pairing (default 8)",
    )
    args = ap.parse_args()
    half = max(0.002, args.period_ms / 1000.0)

    stop = {"flag": False}

    def _sig(_s, _f) -> None:
        stop["flag"] = True

    signal.signal(signal.SIGINT, _sig)
    signal.signal(signal.SIGTERM, _sig)

    print("hs_j19_pair_scan: drive one, listen others (Ctrl-C abort)", flush=True)
    print("  FPC pins 7–8: no GPIO — skipped as drivers/listeners", flush=True)
    for pin, bcm, lab in J19_LINES:
        if bcm is None:
            print(f"  pin{pin}  —     {lab}", flush=True)
        else:
            print(f"  pin{pin}  BCM{bcm:<2d}  {lab}", flush=True)

    # matrix[drive_bcm][listen_bcm] = edge_count
    matrix: dict[int, dict[int, int]] = {d: {l: 0 for l in GPIO_BCMS if l != d} for d in GPIO_BCMS}
    drive_edges: dict[int, int] = {d: 0 for d in GPIO_BCMS}

    try:
        for pin, drive_bcm, lab in J19_LINES:
            if stop["flag"]:
                break
            if drive_bcm is None:
                print(f"\n--- pin{pin} {lab}: no GPIO — skip ---", flush=True)
                continue

            print(
                f"\n--- driving pin{pin} BCM{drive_bcm} ({lab}) "
                f"for {args.dwell_s:.1f}s ---",
                flush=True,
            )
            listeners = [b for b in GPIO_BCMS if b != drive_bcm]
            for b in listeners:
                _set_input_no_bias(b)
            set_output_level(drive_bcm, False)

            last = {b: _level_hi(b) for b in listeners}
            last[drive_bcm] = False
            t_end = time.monotonic() + args.dwell_s
            next_flip = time.monotonic() + half
            level = False

            while time.monotonic() < t_end and not stop["flag"]:
                now = time.monotonic()
                if now >= next_flip:
                    level = not level
                    set_output_level(drive_bcm, level)
                    drive_edges[drive_bcm] += 1
                    next_flip = now + half
                    # Sample listeners right after drive edge
                    time.sleep(0.0005)
                    for b in listeners:
                        cur = _level_hi(b)
                        if cur is None:
                            continue
                        if last[b] is not None and cur != last[b]:
                            matrix[drive_bcm][b] += 1
                        last[b] = cur
                else:
                    time.sleep(0.0005)

            # Release this driver before next
            _set_input_no_bias(drive_bcm)
            hits = {
                b: c
                for b, c in matrix[drive_bcm].items()
                if c >= args.min_edges
            }
            if hits:
                parts = [
                    f"BCM{b}({_lab(b)})={c}" for b, c in sorted(hits.items(), key=lambda x: -x[1])
                ]
                print(
                    f"  drive edges={drive_edges[drive_bcm]}  "
                    f"paired: {', '.join(parts)}",
                    flush=True,
                )
            else:
                print(
                    f"  drive edges={drive_edges[drive_bcm]}  "
                    f"no listen pin ≥{args.min_edges} edges "
                    f"(raw={matrix[drive_bcm]})",
                    flush=True,
                )
    finally:
        print("\nRestoring all GPIOs to input + pull-up…", flush=True)
        for b in GPIO_BCMS:
            try:
                set_input_pull_up(b)
            except MapperError:
                pass

    print("\n=== pairing summary (drive → listen edges) ===", flush=True)
    hdr = "drive\\listen" + "".join(f"  {b:>3d}" for b in GPIO_BCMS)
    print(hdr, flush=True)
    for d in GPIO_BCMS:
        row = f"BCM{d:<2d} ({_lab(d):8s})"
        for l in GPIO_BCMS:
            if l == d:
                row += "    ·"
            else:
                row += f"  {matrix[d][l]:3d}"
        print(row, flush=True)

    print("\n=== inferred pairs (mutual or one-way ≥ min_edges) ===", flush=True)
    reported: set[tuple[int, int]] = set()
    for d in GPIO_BCMS:
        for l, c in matrix[d].items():
            if c < args.min_edges:
                continue
            a, b = sorted((d, l))
            if (a, b) in reported:
                continue
            reported.add((a, b))
            back = matrix[l].get(d, 0)
            print(
                f"  BCM{d} ({_lab(d)}) ↔ BCM{l} ({_lab(l)}): "
                f"fwd={c} back={back}",
                flush=True,
            )
    if not reported:
        print("  (none — flex unplugged, jumpers off PI, or no coupling)", flush=True)
    return 0


def _lab(bcm: int) -> str:
    for _, b, lab in J19_LINES:
        if b == bcm:
            return lab
    return "?"


if __name__ == "__main__":
    raise SystemExit(main())
