# -*- coding: utf-8 -*-
# gui/components/model_view/_interaction_scenario.py
"""Базовый сценарий взаимодействия и менеджер сценариев.

Архитектура:
- InteractionScenario — базовый класс с методами-хуками для событий.
- CameraPolicy — декларативная политика камеры для сценария.
- InteractionManager — стек сценариев + делегация событий.
"""

from __future__ import annotations

from enum import Enum, auto
from typing import TYPE_CHECKING

from PySide6.QtCore import Qt, QEvent
from PySide6.QtGui import QGuiApplication
from error_logger import log_exception

if TYPE_CHECKING:  # pragma: no cover
    from gui.components.model_view_widget import ModelViewWidget


# ── CameraPolicy ────────────────────────────────────────────────────────────

class CameraPolicy(Enum):
    """Политика камеры для сценария."""
    FREE = auto()       # ПКМ вращение, СКМ панорамирование
    TOP_VIEW = auto()   # Только pan/zoom, без вращения
    LOCKED = auto()     # Камера заблокирована


# ── InteractionScenario ─────────────────────────────────────────────────────

class InteractionScenario:
    """Базовый класс сценария взаимодействия.

    Каждый конкретный сценарий переопределяет нужные хуки.
    Менеджер передаёт события текущему сценарию через эти методы.
    """

    name: str = ""
    camera_policy: CameraPolicy = CameraPolicy.FREE

    def on_activate(self, mv: "ModelViewWidget") -> None:
        """Вызывается при входе в сценарий (push/replace)."""

    def on_deactivate(self, mv: "ModelViewWidget") -> None:
        """Вызывается при выходе из сценария (pop/replace/reset)."""

    def on_mouse_press(self, mv: "ModelViewWidget", event) -> bool:
        """ЛКМ/ПКМ/СКМ нажатие. Вернуть True если событие поглощено."""
        return False

    def on_mouse_move(self, mv: "ModelViewWidget", event) -> bool:
        """Движение мыши (с нажатой кнопкой). Вернуть True если поглощено."""
        return False

    def on_mouse_release(self, mv: "ModelViewWidget", event) -> bool:
        """Отпускание кнопки мыши. Вернуть True если поглощено."""
        return False

    def on_hover(self, mv: "ModelViewWidget", event) -> None:
        """Движение мыши без нажатых кнопок (hover)."""

    def on_key_press(self, mv: "ModelViewWidget", event) -> bool:
        """Нажатие клавиши. Вернуть True если поглощено."""
        return False

    def on_resume(self, mv: "ModelViewWidget") -> None:
        """Вызывается когда сценарий снова стал верхним (после pop вышестоящего).

        По умолчанию вызывает on_activate повторно, чтобы восстановить обработчики.
        """
        self.on_activate(mv)

    def on_widget_mouse_press(self, mv: "ModelViewWidget", event) -> bool:
        """mousePressEvent виджета (не eventFilter). Вернуть True если поглощено."""
        return False

    def on_widget_mouse_move(self, mv: "ModelViewWidget", event) -> bool:
        """mouseMoveEvent виджета. Вернуть True если поглощено."""
        return False

    def on_widget_mouse_release(self, mv: "ModelViewWidget", event) -> bool:
        """mouseReleaseEvent виджета. Вернуть True если поглощено."""
        return False


# ── InteractionManager ──────────────────────────────────────────────────────

class InteractionManager:
    """Стек сценариев взаимодействия.

    - Всегда есть base-layer (камерный сценарий) — он не в стеке.
    - Стек содержит доменные сценарии (contour_edit, rack_placement...).
    - Верхний элемент стека — текущий сценарий.
    - ESC → pop() → автоматический on_deactivate().
    - reset() → очищает весь стек.
    """

    def __init__(self, mv: "ModelViewWidget") -> None:
        self._mv = mv
        self._stack: list[InteractionScenario] = []
        self._camera: InteractionScenario | None = None

    # -- Публичный API --------------------------------------------------------

    def set_camera_scenario(self, scenario: InteractionScenario) -> None:
        """Установить базовый камерный сценарий (всегда активен)."""
        self._camera = scenario

    @property
    def current(self) -> InteractionScenario | None:
        """Текущий (верхний) доменный сценарий, или None."""
        return self._stack[-1] if self._stack else None

    @property
    def current_name(self) -> str:
        """Имя текущего сценария."""
        top = self.current
        return top.name if top else ""

    def is_active(self, name: str) -> bool:
        """Проверить, есть ли сценарий с данным именем в стеке."""
        key = str(name)
        return any(s.name == key for s in self._stack)

    def push(self, scenario: InteractionScenario) -> None:
        """Войти в новый сценарий (добавить на вершину стека)."""
        scenario.on_activate(self._mv)
        self._stack.append(scenario)

    def pop(self) -> InteractionScenario | None:
        """Выйти из текущего сценария, вернуть его."""
        if not self._stack:
            return None
        scenario = self._stack.pop()
        scenario.on_deactivate(self._mv)
        # Восстановить обработчики нижележащего сценария
        new_top = self.current
        if new_top is not None:
            new_top.on_resume(self._mv)
        return scenario

    def pop_by_name(self, name: str) -> InteractionScenario | None:
        """Удалить сценарий по имени (из любого места стека)."""
        key = str(name)
        for i, s in enumerate(self._stack):
            if s.name == key:
                was_top = (i == len(self._stack) - 1)
                scenario = self._stack.pop(i)
                scenario.on_deactivate(self._mv)
                if was_top:
                    new_top = self.current
                    if new_top is not None:
                        new_top.on_resume(self._mv)
                return scenario
        return None

    def replace(self, scenario: InteractionScenario) -> InteractionScenario | None:
        """Заменить текущий сценарий новым (без промежуточного on_resume)."""
        old = None
        if self._stack:
            old = self._stack.pop()
            old.on_deactivate(self._mv)
        self.push(scenario)
        return old

    def reset(self) -> None:
        """Сбросить весь стек (ESC-уровень)."""
        while self._stack:
            scenario = self._stack.pop()
            try:
                scenario.on_deactivate(self._mv)
            except Exception as e:
                log_exception(__name__, "reset", e)

    # -- Диспетчеризация событий eventFilter ----------------------------------

    def dispatch_event_filter(self, watched, event) -> bool | None:
        """Маршрутизация события из eventFilter.

        Returns:
            True  — событие поглощено.
            False — событие НЕ должно обрабатываться дальше.
            None  — передать в super().eventFilter().
        """
        top = self.current
        etype = event.type()

        # 1. Нажатие клавиши → текущий сценарий
        if etype == QEvent.Type.KeyPress:
            if top and top.on_key_press(self._mv, event):
                return True

        # 2. Mouse press/move/release → текущий сценарий, затем камера
        if etype == QEvent.Type.MouseButtonPress:
            if top and top.on_mouse_press(self._mv, event):
                return True
            if self._camera and self._dispatch_camera_press(event):
                return True

        if etype == QEvent.Type.MouseMove:
            # robust-классификация drag vs hover:
            # учитываем Qt event buttons, глобальные кнопки и внутренние drag-флаги камеры.
            ev_buttons = Qt.MouseButton.NoButton
            global_buttons = Qt.MouseButton.NoButton
            try:
                if hasattr(event, "buttons"):
                    ev_buttons = event.buttons()
            except Exception as e:
                log_exception(__name__, "dispatch_event_filter", e)
                ev_buttons = Qt.MouseButton.NoButton
            try:
                global_buttons = QGuiApplication.mouseButtons()
            except Exception as e:
                log_exception(__name__, "dispatch_event_filter", e)
                global_buttons = Qt.MouseButton.NoButton
            has_buttons = bool(
                ev_buttons
                or global_buttons
                or getattr(self._mv, "_cam_rotate_active", False)
                or getattr(self._mv, "_cam_pan_active", False)
            )
            if has_buttons:
                # Движение с зажатой кнопкой
                if top and top.on_mouse_move(self._mv, event):
                    return True
                cam_result = self._dispatch_camera_move(event)
                if cam_result is not None:
                    return cam_result
                # Не отдаём drag-события в VTK-style, чтобы исключить побочный захват камеры.
                return False
            else:
                # Hover (без кнопок)
                if top:
                    top.on_hover(self._mv, event)
                    # В origin-flow hover нужен для preview-маркера,
                    # но не должен уходить в VTK interactor style.
                    if top.name == "origin_point":
                        # Защита от ложного swallow после завершения origin-сценария:
                        # поглощаем hover только пока реально подключены origin-hover handlers.
                        has_hover_handlers = bool(
                            getattr(self._mv, "_hover_screen_handler", None)
                            or getattr(self._mv, "_hover_handler", None)
                        )
                        if has_hover_handlers:
                            return True
                # Камера не обрабатывает hover

        if etype == QEvent.Type.MouseButtonRelease:
            if top and top.on_mouse_release(self._mv, event):
                return True
            cam_result = self._dispatch_camera_release(event)
            if cam_result is not None:
                return cam_result

        return None

    # -- Диспетчеризация mousePressEvent/Move/Release виджета -----------------

    def dispatch_widget_mouse_press(self, event) -> bool:
        """Делегация mousePressEvent виджета текущему сценарию."""
        top = self.current
        if top and top.on_widget_mouse_press(self._mv, event):
            return True
        return False

    def dispatch_widget_mouse_move(self, event) -> bool:
        """Делегация mouseMoveEvent виджета текущему сценарию."""
        top = self.current
        if top and top.on_widget_mouse_move(self._mv, event):
            return True
        return False

    def dispatch_widget_mouse_release(self, event) -> bool:
        """Делегация mouseReleaseEvent виджета текущему сценарию."""
        top = self.current
        if top and top.on_widget_mouse_release(self._mv, event):
            return True
        return False

    # -- Камерные делегации ---------------------------------------------------

    def _dispatch_camera_press(self, event) -> bool:
        if not self._camera:
            return False
        if getattr(self._mv, "_camera_locked", False):
            return False
        top = self.current
        policy = top.camera_policy if top else CameraPolicy.FREE
        if policy == CameraPolicy.LOCKED:
            return False
        # TOP_VIEW: блокировать вращение (ПКМ), разрешить панорамирование (СКМ)
        if policy == CameraPolicy.TOP_VIEW:
            if hasattr(event, "button") and event.button() == Qt.MouseButton.RightButton:
                # Поглощаем ПКМ, чтобы событие не ушло в VTK-style (иначе возможен zoom/pan).
                return True
        return self._camera.on_mouse_press(self._mv, event)

    def _dispatch_camera_move(self, event) -> bool | None:
        if not self._camera:
            return None
        if getattr(self._mv, "_camera_locked", False):
            return None
        top = self.current
        policy = top.camera_policy if top else CameraPolicy.FREE
        if policy == CameraPolicy.LOCKED:
            return None
        return self._camera.on_mouse_move(self._mv, event) or None

    def _dispatch_camera_release(self, event) -> bool | None:
        if not self._camera:
            return None
        if getattr(self._mv, "_camera_locked", False):
            return None
        top = self.current
        policy = top.camera_policy if top else CameraPolicy.FREE
        if policy == CameraPolicy.TOP_VIEW:
            if hasattr(event, "button") and event.button() == Qt.MouseButton.RightButton:
                return True
        return self._camera.on_mouse_release(self._mv, event) or None
