#!/usr/bin/env python3
"""Blink POG-COM-PI-HAT HS OUT nets from Seeed J19 (Pi GPIO → level shifter).

Bench bring-up after PCB rework: drive M-S_OUT and OIS_OUT so HAT LEDs show
the Pi can toggle the OUT path before IN sense lines are restored.

Defaults — Rev E / verified FPC map (HS_GPIO_TAP.md):
  FPC pin 1 → BCM11 / GPIO11   M-S_OUT   500 ms
  FPC pin 3 → BCM10 / GPIO10   OIS_OUT   250 ms

  cd ~/EtherGold/sync
  sudo ./run_hs_out_blink.sh
  # Ctrl-C restores lines to input pull-up

3.3 V GPIO only.
"""

from __future__ import annotations

import argparse
import signal
import sys
import time

from j19_io_mapper import MapperError, pinctrl_get, set_input_pull_up, set_output_level

# Rev E FPC: pin1=BCM11 M-S_OUT, pin3=BCM10 OIS_OUT.
DEFAULT_MS_OUT_BCM = 11
DEFAULT_OIS_OUT_BCM = 10
DEFAULT_MS_PERIOD_MS = 500
DEFAULT_OIS_PERIOD_MS = 250
DEFAULT_ALL_PERIOD_MS = 250

# Verified J19 phys pins (j19_io_mapper / HS_GPIO_TAP.md).
J19_ALL_GPIO: list[tuple[int, int, 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/HW_HS"),
]


def _duty_high_ms(period_ms: int) -> int:
    return max(1, period_ms // 2)


def main() -> int:
    ap = argparse.ArgumentParser(description="Blink M-S_OUT and OIS_OUT on J19")
    ap.add_argument(
        "--ms-bcm",
        type=int,
        default=DEFAULT_MS_OUT_BCM,
        help="M-S path BCM (default 11 Rev E FPC pin1)",
    )
    ap.add_argument(
        "--ois-bcm",
        type=int,
        default=DEFAULT_OIS_OUT_BCM,
        help="OIS path BCM (default 10 Rev E FPC pin3; use -1 to disable)",
    )
    ap.add_argument(
        "--ms-period-ms",
        type=int,
        default=DEFAULT_MS_PERIOD_MS,
        help="M-S_OUT full period ms (default 500 → 250/250)",
    )
    ap.add_argument(
        "--ois-period-ms",
        type=int,
        default=DEFAULT_OIS_PERIOD_MS,
        help="OIS_OUT full period ms (default 250 → 125/125)",
    )
    ap.add_argument(
        "--ms-only",
        action="store_true",
        help="Blink only M-S path",
    )
    ap.add_argument(
        "--ois-only",
        action="store_true",
        help="Blink only OIS path",
    )
    ap.add_argument(
        "--all-j19",
        action="store_true",
        help="Drive every J19 GPIO (BCM 11/9/10/8/2/3) at the same period",
    )
    ap.add_argument(
        "--all-period-ms",
        type=int,
        default=DEFAULT_ALL_PERIOD_MS,
        help="Period for --all-j19 (default 250)",
    )
    args = ap.parse_args()

    if args.ms_only and args.ois_only:
        print("FATAL: --ms-only and --ois-only are mutually exclusive", file=sys.stderr)
        return 2
    if args.all_j19 and (args.ms_only or args.ois_only):
        print("FATAL: --all-j19 cannot combine with --ms-only/--ois-only", file=sys.stderr)
        return 2

    lines: list[tuple[str, int, int]] = []
    if args.all_j19:
        if args.all_period_ms < 2:
            print("FATAL: --all-period-ms must be >= 2", file=sys.stderr)
            return 2
        for pin_i, bcm, label in J19_ALL_GPIO:
            lines.append((f"p{pin_i}/{label}", bcm, args.all_period_ms))
    else:
        if (not args.ois_only and args.ms_period_ms < 2) or (
            not args.ms_only and args.ois_bcm >= 0 and args.ois_period_ms < 2
        ):
            print("FATAL: periods must be >= 2 ms", file=sys.stderr)
            return 2
        if not args.ois_only:
            lines.append(("M-S", args.ms_bcm, args.ms_period_ms))
        if not args.ms_only and args.ois_bcm >= 0:
            lines.append(("OIS", args.ois_bcm, args.ois_period_ms))
    if not lines:
        print("FATAL: nothing to blink", file=sys.stderr)
        return 2
    bcms = [bcm for _, bcm, _ in lines]

    print("hs_out_blink: Pi GPIO → HAT HS OUT (Ctrl-C to stop)", flush=True)
    for name, bcm, period in lines:
        high = _duty_high_ms(period)
        print(
            f"  {name:8s}  BCM{bcm:<3d}  period={period} ms  "
            f"high={high} ms low={period - high} ms  ({pinctrl_get(bcm)})",
            flush=True,
        )

    stop = {"flag": False}

    def _on_signal(_signum, _frame) -> None:
        stop["flag"] = True

    signal.signal(signal.SIGINT, _on_signal)
    signal.signal(signal.SIGTERM, _on_signal)

    # Independent phases: track next edge time and current level per line.
    now = time.monotonic()
    state: list[dict] = []
    for name, bcm, period in lines:
        high_s = _duty_high_ms(period) / 1000.0
        low_s = (period - _duty_high_ms(period)) / 1000.0
        try:
            set_output_level(bcm, True)
        except MapperError as exc:
            print(f"FATAL: {exc}", file=sys.stderr)
            for b in bcms:
                try:
                    set_input_pull_up(b)
                except MapperError:
                    pass
            return 1
        state.append(
            {
                "name": name,
                "bcm": bcm,
                "high": True,
                "high_s": high_s,
                "low_s": low_s,
                "next": now + high_s,
            }
        )

    try:
        while not stop["flag"]:
            now = time.monotonic()
            soonest = min(s["next"] for s in state)
            delay = soonest - now
            if delay > 0:
                time.sleep(min(delay, 0.05))
                continue
            for s in state:
                if now + 0.0005 < s["next"]:
                    continue
                s["high"] = not s["high"]
                set_output_level(s["bcm"], s["high"])
                s["next"] = now + (s["high_s"] if s["high"] else s["low_s"])
    finally:
        print("\nhs_out_blink: restore inputs (pull-up)")
        for bcm in bcms:
            try:
                set_input_pull_up(bcm)
                print(f"  BCM{bcm}: {pinctrl_get(bcm)}")
            except MapperError as exc:
                print(f"  BCM{bcm}: restore failed: {exc}", file=sys.stderr)

    return 0


if __name__ == "__main__":
    raise SystemExit(main())
