# event_captor.py
"""
Hooks souris pour agent_v0.

v0+ :
- écoute des clics souris globaux
- écoute du scroll (molette)
- détecte les périodes d'immobilité (hover) > hover_min_idle_ms
"""

from __future__ import annotations

import threading
import time
from typing import Callable, Optional

from pynput import mouse

MouseClickCallback = Callable[[str, int, int], None]
MouseScrollCallback = Callable[[int, int, int, int], None]  # x, y, dx, dy
MouseHoverCallback = Callable[[int, int, int], None]        # x, y, idle_ms


class EventCaptor:
    """
    Capture les événements souris globaux et les relaie à des callbacks.
    """

    def __init__(
        self,
        on_mouse_click: MouseClickCallback,
        on_scroll: Optional[MouseScrollCallback] = None,
        on_hover: Optional[MouseHoverCallback] = None,
        hover_min_idle_ms: int = 700,
    ) -> None:
        """
        :param on_mouse_click: fonction appelée sur clic souris,
                               signature (button_str, x, y)
        :param on_scroll: fonction appelée sur scroll (molette), ou None
                          signature (x, y, dx, dy)
        :param on_hover: fonction appelée quand la souris est immobile
                         depuis hover_min_idle_ms, ou None
                         signature (x, y, idle_ms)
        """
        self._on_mouse_click = on_mouse_click
        self._on_scroll = on_scroll
        self._on_hover = on_hover
        self._hover_min_idle_ms = hover_min_idle_ms

        self._listener: mouse.Listener | None = None

        # Pour le suivi du déplacement souris (hover)
        self._last_move_time: Optional[float] = None
        self._last_move_pos: Optional[tuple[int, int]] = None

        self._hover_thread: Optional[threading.Thread] = None
        self._hover_thread_running: bool = False

    def start(self) -> None:
        """Démarre l'écoute globale des événements souris."""
        if self._listener is not None:
            return

        self._listener = mouse.Listener(
            on_click=self._handle_click,
            on_scroll=self._handle_scroll,
            on_move=self._handle_move,
        )
        self._listener.start()

        if self._on_hover is not None:
            self._hover_thread_running = True
            self._hover_thread = threading.Thread(
                target=self._hover_loop, name="HoverDetector", daemon=True
            )
            self._hover_thread.start()

    def stop(self) -> None:
        """Arrête l'écoute globale et le thread hover."""
        if self._listener is not None:
            self._listener.stop()
            self._listener = None

        self._hover_thread_running = False
        self._hover_thread = None

        self._last_move_time = None
        self._last_move_pos = None

    # --- internes ---

    def _handle_click(self, x: int, y: int, button: mouse.Button, pressed: bool) -> None:
        # On ne loggue que l'événement "pressed"
        if not pressed:
            return

        button_str = getattr(button, "name", str(button))
        self._on_mouse_click(button_str, x, y)

    def _handle_scroll(self, x: int, y: int, dx: int, dy: int) -> None:
        if self._on_scroll is not None:
            self._on_scroll(x, y, dx, dy)

    def _handle_move(self, x: int, y: int) -> None:
        now = time.monotonic()
        self._last_move_time = now
        self._last_move_pos = (x, y)

    def _hover_loop(self) -> None:
        """
        Thread qui surveille l'immobilité de la souris.
        Si la souris reste immobile plus de hover_min_idle_ms,
        on déclenche un callback on_hover(x, y, idle_ms).
        """
        last_hover_trigger_time: float | None = None

        while self._hover_thread_running:
            time.sleep(0.1)

            if self._on_hover is None:
                continue

            if self._last_move_time is None or self._last_move_pos is None:
                continue

            now = time.monotonic()
            idle_ms = int((now - self._last_move_time) * 1000)

            if idle_ms < self._hover_min_idle_ms:
                continue

            # Éviter de déclencher en boucle pour la même "immobilité"
            if last_hover_trigger_time is not None:
                # Si on a déjà déclenché récemment, on attend un nouveau mouvement
                if self._last_move_time <= last_hover_trigger_time:
                    continue

            x, y = self._last_move_pos
            self._on_hover(x, y, idle_ms)
            last_hover_trigger_time = now
