"""Bounded USB serial protocol used by Learning Lab projects.

Install pyserial in a virtual environment. This file controls real hardware
only when the caller supplies a port. No network or cloud service is used.
"""
import time


class Bridge:
    ALLOWED = {"LED_ON", "LED_OFF", "STOP", "FORWARD", "LEFT", "RIGHT"}

    def __init__(self, port):
        import serial
        self.serial = serial.Serial(port, 115200, timeout=0.05, write_timeout=0.3)
        # Uno R3 commonly resets when its serial port opens.
        time.sleep(2)
        self.serial.reset_input_buffer()
        self.send("STOP")

    def _request(self, command, prefix):
        self.serial.write((command + "\n").encode("ascii"))
        deadline = time.monotonic() + 0.4
        while time.monotonic() < deadline:
            line = self.serial.readline(128).decode("ascii", errors="replace").strip()
            if line.startswith(prefix):
                return line
            if line in {"ERROR", "BLOCKED"}:
                raise RuntimeError(f"Board rejected command: {line}")
        raise TimeoutError(f"No acknowledgement for {command}")

    def send(self, command):
        if command not in self.ALLOWED:
            raise ValueError("Command not in the hardware allowlist")
        return self._request(command, "OK " + command)

    def read(self):
        values = self._request("READ", "DATA ").split()
        if len(values) != 4:
            raise ValueError("Expected three sensor values")
        light, temperature, button = map(int, values[1:])
        if not (0 <= light <= 1023 and 0 <= temperature <= 1023 and button in (0, 1)):
            raise ValueError("Sensor value outside protocol range")
        return light, temperature, button

    def hold(self, command, seconds=0.3):
        if not 0 < seconds <= 2:
            raise ValueError("A demonstration action lasts at most two seconds")
        deadline = time.monotonic() + seconds
        try:
            while time.monotonic() < deadline:
                self.send(command)
                time.sleep(0.1)
        finally:
            self.send("STOP")

    def __enter__(self):
        return self

    def __exit__(self, *_):
        try:
            self.send("STOP")
        finally:
            self.serial.close()
