#!/usr/bin/env python3
"""
╔═══════════════════════════════════════════════════════════╗
║              LID SYNTH BRIDGE  v1.0                       ║
║  Reads your MacBook's lid angle sensor (real hardware)    ║
║  and streams values to the browser via WebSocket.         ║
╠═══════════════════════════════════════════════════════════╣
║  SETUP (first time only):                                 ║
║    pip3 install websockets                                ║
║                                                           ║
║  RUN:                                                     ║
║    python3 lid-bridge.py                                  ║
║                                                           ║
║  Then open lid-synth.html in your browser.                ║
║  Compatible: MacBook Pro 2019+ / MacBook Air 2020+        ║
╚═══════════════════════════════════════════════════════════╝
"""

import asyncio
import json
import os
import subprocess
import sys

try:
    from websockets.asyncio.server import serve as ws_serve
except ImportError:
    try:
        from websockets.server import serve as ws_serve
    except ImportError:
        print("✗  websockets not installed.  Run:  pip3 install websockets")
        sys.exit(1)

# ── Config ────────────────────────────────────────────────────────────────
PORT        = 9731
HERE        = os.path.dirname(os.path.abspath(__file__))
SWIFT_SRC   = os.path.join(HERE, "lid-sensor.swift")
BINARY      = os.path.join(HERE, ".lid-sensor-bin")

# ── Shared state ──────────────────────────────────────────────────────────
current_angle: float = 0.0        # degrees
connected: set        = set()

# ── Compile the Swift sensor reader (cached) ──────────────────────────────
def ensure_binary():
    need = (not os.path.exists(BINARY) or
            os.path.getmtime(SWIFT_SRC) > os.path.getmtime(BINARY))
    if not need:
        return True

    print("⚙   Compiling sensor reader (one-time, ~10 s)…", flush=True)
    r = subprocess.run(
        ["swiftc", SWIFT_SRC, "-o", BINARY, "-O"],
        capture_output=True, text=True
    )
    if r.returncode != 0:
        print("✗  Swift compilation failed:\n" + r.stderr)
        return False
    print("✓  Compiled successfully.", flush=True)
    return True

# ── WebSocket handler ─────────────────────────────────────────────────────
async def handler(ws):
    connected.add(ws)
    print(f"↗  Browser connected  ({len(connected)} total)", flush=True)
    try:
        # Send current angle immediately so the page doesn't wait
        await ws.send(json.dumps({"angle": round(current_angle, 2)}))
        await ws.wait_closed()
    finally:
        connected.discard(ws)
        print(f"↘  Browser disconnected  ({len(connected)} remaining)", flush=True)

# ── Sensor broadcast loop ─────────────────────────────────────────────────
async def sensor_loop():
    global current_angle

    print(f"▶  Starting sensor reader…", flush=True)
    proc = await asyncio.create_subprocess_exec(
        BINARY,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )

    # First line from stderr tells us if the sensor was found
    stderr_line = await asyncio.wait_for(proc.stderr.readline(), timeout=5.0)
    line = stderr_line.decode().strip()
    if line.startswith("OK:"):
        print(f"✓  Sensor found: {line[3:]}", flush=True)
        print(f"\n   WebSocket running on ws://localhost:{PORT}", flush=True)
        print(f"   Open lid-synth.html in your browser.\n", flush=True)
    elif line.startswith("ERR:"):
        print(f"\n✗  {line[4:]}")
        print("   Is this a MacBook Pro (2019+) or MacBook Air (2020+)?")
        print("   Some models are not supported.\n")
        proc.kill()
        return

    # Stream angle values
    async for raw_line in proc.stdout:
        try:
            raw = int(raw_line.decode().strip())
            current_angle = float(raw)           # raw value is directly in degrees

            if connected:
                msg = json.dumps({"angle": round(current_angle, 2)})
                await asyncio.gather(
                    *[c.send(msg) for c in connected.copy()],
                    return_exceptions=True
                )
        except ValueError:
            pass

    print("✗  Sensor reader exited unexpectedly.", flush=True)

# ── Main ──────────────────────────────────────────────────────────────────
async def main():
    print(__doc__)

    if not ensure_binary():
        sys.exit(1)

    async with ws_serve(handler, "localhost", PORT):
        await sensor_loop()

asyncio.run(main())
