#!/usr/bin/env python3
"""Local API v1: a serial, bounded tension example. Default: dry run.
Run: python3 controlled.py --run
Requires a purchased Feel My Mac license and the Local API material.
Experimentation/nonprofit API use has no additional fee. For game/program
integration contact business@thehighjack.it.
"""
import argparse
import http.client
import json
import signal
import time
import uuid


class Client:
    def __init__(self, live=False, port=59723):
        self.live = live
        self.port = port
        self.client_id = str(uuid.uuid4())
        self.last_post = float("-inf")
        self.virtual_time = 0.0

    def now(self):
        return time.monotonic() if self.live else self.virtual_time

    def wait(self, seconds):
        if self.live:
            time.sleep(max(0, seconds))
        else:
            self.virtual_time += max(0, seconds)

    def request(self, path, body=None):
        method = "GET" if body is None else "POST"
        if method == "POST":
            self.wait(max(0, 0.05 - (self.now() - self.last_post)))
            self.last_post = self.now()
        payload = None if body is None else json.dumps(body, allow_nan=False).encode()
        if not self.live:
            print(method, path, "" if payload is None else payload.decode())
            if path == "/v1/status":
                return {"ok": True, "outputAvailable": True}
            if path == "/v1/parameters":
                return {"ok": True, "resonance": {"controlled": {}}}
            return {"ok": True}
        headers = {"X-FMM-Client": self.client_id, "X-FMM-Client-Name": "Python example"}
        if payload is not None:
            headers.update({"Content-Type": "application/json", "Content-Length": str(len(payload))})
        # Fixed IPv4 loopback, no redirects, one request at a time, no idle timer.
        connection = http.client.HTTPConnection("127.0.0.1", self.port, timeout=0.8)
        try:
            connection.request(method, path, body=payload, headers=headers)
            response = connection.getresponse()
            raw = response.read(65537)
            if len(raw) > 65536:
                raise RuntimeError("Response exceeds the example's size limit")
            result = json.loads(raw)
            if response.status != 200 or result.get("ok") is not True:
                raise RuntimeError(f"HTTP {response.status}: {result.get('error', 'invalid response')}")
            return result
        finally:
            connection.close()

    def hold(self, seconds):
        """Renew only for this wanted interaction, including while paused."""
        deadline = self.now() + seconds
        while self.now() < deadline:
            self.wait(min(0.5, deadline - self.now()))
            self.request("/v1/heartbeat", {})


def tension(charge):
    t = max(0, min(1, charge)) ** 1.7
    return {"intensity": 0.08 + 0.32 * t, "pulseRateHz": 8 + 32 * t, "sharpness": t}


def demo(client):
    if not client.request("/v1/status").get("outputAvailable"):
        raise RuntimeError("Enable licensed Local API output in Feel My Mac")
    modes = client.request("/v1/parameters").get("resonance", {})
    if "controlled" not in modes:
        raise RuntimeError("This Feel My Mac build does not expose controlled resonance")
    attempted_start = False
    try:
        # Mark the attempt before sending: a timeout can leave the outcome unknown.
        attempted_start = True
        client.request("/v1/resonance", {"mode": "controlled", **tension(0), "transitionMs": 60})
        # A real app coalesces frame/input changes to its latest target, at <=20/s.
        for frame in range(1, 11):
            client.wait(0.05)
            client.request("/v1/resonance/update", tension(frame / 10))
        client.hold(2.5)  # Remains active beyond the 2-second lease while held still.
        client.request("/v1/resonance/pause", {})
        client.request("/v1/resonance/update", {"intensity": 0.15, "sharpness": 0.2})
        client.hold(1.0)  # Paused sessions still need renewal.
        client.request("/v1/resonance/resume", {})
        client.hold(0.5)
    finally:
        if attempted_start:
            client.request("/v1/stop", {})
    # To represent a completed release, optionally send a crisp impulse here,
    # after successful stop. Never do that from finally/cancellation cleanup.


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--run", action="store_true", help="Send commands to Feel My Mac; otherwise print a dry run")
    parser.add_argument("--port", type=int, default=59723, help="IPv4 loopback port; useful with a local mock")
    args = parser.parse_args()
    if not 1 <= args.port <= 65535:
        parser.error("port must be 1...65535")
    def terminate(_signal, _frame):
        raise KeyboardInterrupt()
    signal.signal(signal.SIGTERM, terminate)
    try:
        demo(Client(args.run, args.port))
    except KeyboardInterrupt:
        print("Cancelled; stop was attempted for any started session.")
        return 130
    except (OSError, ValueError, RuntimeError, http.client.HTTPException) as error:
        print(f"Stopped: {error}")
        return 1
    return 0


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