"""SP110E / SP108E addressable-BLE-controller protocol.

The SP110E is a BLE controller for addressable LED strips (WS2811, WS2812B,
SK6812, APA102, etc.). Its phone app (several rebrands, including "LED Hue",
"SP110E", "Custom Lights") streams control commands — but does **not** stream
per-pixel frames. The BLE protocol exposes:

    * pick LED IC type + channel order
    * pick a built-in animation pattern
    * set animation speed + brightness
    * set a single static color for the whole strip

So from LedGrab's perspective, SP110E is a whole-strip ambient controller.

Frame format (4 bytes):

    ``RR GG BB CC``

where ``CC`` is the command byte. Static-color command is ``0x1E`` (set
"RGB" mode = whole-strip solid color from the RR GG BB payload). Power is
a distinct command (``0xAA`` ON / ``0xAB`` OFF, with the three payload
bytes ignored). Brightness is applied by the *caller* scaling the RGB
triple — there is no separate brightness command for solid-color mode,
which is simpler and lets LedGrab apply its own processing pipeline.

Init handshake (critical):
    The controller silently tears the GATT link down within ~1s if a
    two-write handshake doesn't arrive immediately after connect. Omitting
    it is indistinguishable from a successful connect on the Python side —
    the first real write then hangs for 30s until Windows abandons the
    queued packet. The handshake writes live in ``init_writes`` below and
    are executed by ``BLEClient.connect()``.

References:
    * https://github.com/Lehkeda/SP110E_controller (reverse-engineered)
    * https://github.com/sysofwan/ha-sp110e
    * https://gist.github.com/mbullington/37957501a07ad065b67d4e8d39bfe012
      (init handshake documentation)
"""

from __future__ import annotations

from ledgrab.core.devices.ble_protocols import BLEProtocol

_SERVICE_UUID = "0000ffe0-0000-1000-8000-00805f9b34fb"
_WRITE_CHAR_UUID = "0000ffe1-0000-1000-8000-00805f9b34fb"
_INIT_CHAR_UUID = "0000ffe2-0000-1000-8000-00805f9b34fb"

_CMD_SET_COLOR = 0x1E
_CMD_POWER_ON = 0xAA
_CMD_POWER_OFF = 0xAB


def _clamp_byte(value: int) -> int:
    if value < 0:
        return 0
    if value > 255:
        return 255
    return value


def encode_color(r: int, g: int, b: int, brightness: int = 255) -> bytes:
    """Build a "set solid color" frame.

    Applies software brightness by scaling RGB — the SP110E protocol has
    no separate brightness channel for static color mode.
    """
    r = _clamp_byte(r)
    g = _clamp_byte(g)
    b = _clamp_byte(b)
    brightness = _clamp_byte(brightness)
    if brightness != 255:
        r = (r * brightness) // 255
        g = (g * brightness) // 255
        b = (b * brightness) // 255
    return bytes((r, g, b, _CMD_SET_COLOR))


def encode_power(on: bool) -> bytes:
    """Build a power on/off frame."""
    cmd = _CMD_POWER_ON if on else _CMD_POWER_OFF
    return bytes((0x00, 0x00, 0x00, cmd))


PROTOCOL = BLEProtocol(
    family="sp110e",
    display_name="SP110E / SP108E (addressable)",
    service_uuid=_SERVICE_UUID,
    write_char_uuid=_WRITE_CHAR_UUID,
    # SP110E accepts Write Without Response — much lower latency.
    write_with_response=False,
    encode_color=encode_color,
    encode_power=encode_power,
    name_prefixes=("SP110E", "SP108E", "BLE-LED"),
    # Vendor handshake — see module docstring. Without these writes the
    # controller drops the GATT connection ~1s after it opens.
    init_writes=(
        (_INIT_CHAR_UUID, b"\x01\x00"),
        (_WRITE_CHAR_UUID, b"\x01\xb7\xe3\xd5"),
    ),
)
