# SPDX-License-Identifier: Apache-2.0
"""The eye switch (SW3, DTS-61K on pin 16, pull-up, closes to GND): edges in, gestures out.

Gestures are the three the device understands (device.py, ADR-0015):
- `down`: the eye went down. The daemon calls Goldfish.eye_down, which takes the room off (or, as the second
  press of a double press while off, resumes provisionally). The double-press rule lives in device.py only, so
  the site and the firmware can't disagree about it.
- `hold`: still down after `hold` seconds. The ring shows the battery while the eye stays down.
- `up`: the eye came up; `held` is how long it was down. The daemon calls Goldfish.eye_up.

Debounce: an edge opens a burst; the burst ends once the line has been quiet for `debounce` s, and the level then
decides. A press is dated from the first edge of its burst, so contact bounce never shortens a double press or
lengthens a hold. A burst that settles back where it started (a glitch shorter than `debounce`) is no gesture.
"""
from __future__ import annotations

from dataclasses import dataclass, field

from resolution_protocol import parse_duration

from .hw import Edge


@dataclass(frozen=True)
class EyeTimes:
    """Every eye timing in one place. `double` and `hold` come from the profile, as device.py reads them."""
    debounce: float = 0.020      # DTS-61K bounce is not in its datasheet; 20 ms is the usual tact-switch figure (VERIFY on a scope)
    double: float = 0.6          # profile bindings.controls.double; used by device.py, kept here for the sim and tests
    hold: float = 2.0            # profile bindings.controls.hold

    @classmethod
    def from_profile(cls, p) -> "EyeTimes":
        c = p.extra.get("bindings", {}).get("controls", {})
        return cls(double=parse_duration(c.get("double", "PT0.6S")), hold=parse_duration(c.get("hold", "PT2S")))


@dataclass(frozen=True)
class Gesture:
    kind: str                    # down | hold | up
    t: float
    held: float = 0.0


@dataclass
class Eye:
    times: EyeTimes = field(default_factory=EyeTimes)
    active_low: bool = True
    down_at: float | None = None          # set while the eye is down
    _held_sent: bool = False
    _burst: tuple | None = None           # (first edge t, last edge t, level now)

    def _pressed(self, level: bool) -> bool:
        return level != self.active_low

    def feed(self, edges: list[Edge]) -> None:
        for e in edges:
            first = self._burst[0] if self._burst else e.t
            self._burst = (first, e.t, e.level)

    def poll(self, now: float) -> list[Gesture]:
        out: list[Gesture] = []
        release = None
        if self._burst and self.down_at is not None and not self._pressed(self._burst[2]):
            release = self._burst[0]                       # a release is under way: holds before it still count
        if self.down_at is not None and not self._held_sent:
            at = self.down_at + self.times.hold
            if (release is None and now >= at) or (release is not None and release >= at):
                out.append(Gesture("hold", at))
                self._held_sent = True
        if self._burst and now - self._burst[1] >= self.times.debounce:
            t0, _, level = self._burst
            self._burst = None
            pressed = self._pressed(level)
            if pressed and self.down_at is None:
                self.down_at, self._held_sent = t0, False
                out.append(Gesture("down", t0))
            elif not pressed and self.down_at is not None:
                out.append(Gesture("up", t0, t0 - self.down_at))
                self.down_at = None
        return out

    def step(self, edges: list[Edge], now: float) -> list[Gesture]:
        self.feed(edges)
        return self.poll(now)


def press_edges(t: float, held: float = 0.1, bounce: int = 0, gap: float = 0.002) -> list[tuple[float, bool]]:
    """The edges of one press on an active-low line, with `bounce` extra chatter pairs at each contact change.
    For the sim and tests: SimInput.at(*e) for each."""
    out = [(t, False)]
    for k in range(bounce):
        out += [(t + (2 * k + 1) * gap, True), (t + (2 * k + 2) * gap, False)]
    up = t + held
    out.append((up, True))
    for k in range(bounce):
        out += [(up + (2 * k + 1) * gap, False), (up + (2 * k + 2) * gap, True)]
    return out
