"""luz.py — a luz do teclado reage ao estado do Primo.

MacBook: o teclado embutido só tem brilho (branco). O método: o framework
privado CoreBrightness (KeyboardBrightnessClient) via PyObjC, carregado em
tempo de execução; guarda o brilho e o modo automático da pessoa e restaura no
fim. Teclado RGB externo: OpenRGB SDK (servidor local, porta 6742), se houver
`openrgb` em PATH e o módulo `openrgb` instalado. Sem suporte, ou com
PRIMOCODE_SEM_LUZ=1 / "luz": false em voz.json, tudo vira silêncio.

    ouvindo = respira · pensando = pulso duplo · falando = segue a voz · erro = pisca
"""
from __future__ import annotations

import math
import os
import sys
import threading
import time
from typing import Optional


def respira(t: float, periodo: float = 4.0, base: float = 0.10, topo: float = 0.45) -> float:
    return base + (topo - base) * (0.5 - 0.5 * math.cos(t / periodo * math.tau))


def pulso(t: float, periodo: float = 1.1, base: float = 0.08, topo: float = 0.8) -> float:
    """dois toques curtos e uma pausa (tipo coração apressado: o Primo está pensando)."""
    x = (t % periodo) / periodo
    forca = max(math.exp(-((x - 0.1) ** 2) / 0.002), 0.7 * math.exp(-((x - 0.3) ** 2) / 0.002))
    return base + (topo - base) * forca


def alvo(estado: str, t: float, nivel: float = 0.0, erro_ate: float = 0.0, agora: float = 0.0) -> Optional[float]:
    """Brilho 0..1 que o estado pede agora (None = não mexer)."""
    if erro_ate > agora:
        return 0.9 if int((erro_ate - agora) * 8) % 2 == 0 else 0.0
    if estado == "ouvindo":
        return respira(t)
    if estado in ("pensando", "executando"):
        return pulso(t)
    if estado == "falando":
        return 0.15 + 0.85 * min(1.0, nivel)
    if estado == "interrompido":
        return 0.7
    return 0.0 if estado == "dormindo" else 0.12


class _Mac:
    def __init__(self) -> None:
        import objc
        from Foundation import NSBundle
        NSBundle.bundleWithPath_("/System/Library/PrivateFrameworks/CoreBrightness.framework").load()
        self._c = objc.lookUpClass("KeyboardBrightnessClient").alloc().init()
        ids = self._c.copyKeyboardBacklightIDs() or []
        if not ids:
            raise RuntimeError("sem luz de teclado")
        self._k = ids[0]
        self.antes = float(self._c.brightnessForKeyboard_(self._k))
        self.auto = bool(self._c.isAutoBrightnessEnabledForKeyboard_(self._k))
        self._c.enableAutoBrightness_forKeyboard_(False, self._k)

    def definir(self, v: float) -> None:
        self._c.setBrightness_fadeSpeed_commit_forKeyboard_(min(1.0, max(0.0, v)), 60, False, self._k)

    def restaurar(self) -> None:
        self._c.setBrightness_fadeSpeed_commit_forKeyboard_(self.antes, 200, False, self._k)
        self._c.enableAutoBrightness_forKeyboard_(self.auto, self._k)


class _OpenRGB:
    """Teclado RGB externo pelo servidor do OpenRGB. Cor ciano, intensidade = brilho."""

    def __init__(self) -> None:
        from openrgb import OpenRGBClient
        from openrgb.utils import RGBColor
        self._cor = RGBColor
        self._cli = OpenRGBClient(name="Primo")
        self._tec = [d for d in self._cli.devices if "keyboard" in str(d.type).lower()]
        if not self._tec:
            raise RuntimeError("sem teclado RGB")

    def definir(self, v: float) -> None:
        for d in self._tec:
            d.set_color(self._cor(int(56 * v), int(225 * v), int(255 * v)))

    def restaurar(self) -> None:
        for d in self._tec:
            d.set_mode("direct")


class Luz:
    def __init__(self, ligada: bool = True) -> None:
        self.dev = None
        self.estado = "esperando"
        self.nivel = 0.0
        self._erro_ate = 0.0
        self._vivo = True
        if not ligada or os.environ.get("PRIMOCODE_SEM_LUZ") == "1":
            return
        for fabrica in ((_Mac,) if sys.platform == "darwin" else ()) + (_OpenRGB,):
            try:
                self.dev = fabrica()
                break
            except Exception:
                continue
        if self.dev:
            threading.Thread(target=self._animar, daemon=True).start()

    def mudar(self, estado: str, nivel: Optional[float] = None) -> None:
        self.estado = estado
        if nivel is not None:
            self.nivel = nivel

    def erro(self) -> None:
        self._erro_ate = time.time() + 0.9

    def _animar(self) -> None:
        t0 = time.time()
        while self._vivo:
            agora = time.time()
            v = alvo(self.estado, agora - t0, self.nivel, self._erro_ate, agora)
            if v is not None:
                try:
                    self.dev.definir(v)
                except Exception:
                    self.dev = None
                    return
            time.sleep(1 / 20)

    def fechar(self) -> None:
        self._vivo = False
        if self.dev:
            time.sleep(0.1)
            try:
                self.dev.restaurar()
            except Exception:
                pass
