# -*- coding: utf-8 -*-
# gui/components/model_view/_mv_interaction_nav.py
# Обработчики кликов и навигация: клики на сцене, масштабирование, панорамирование, колесо мыши

from __future__ import annotations

import math
import time
from typing import TYPE_CHECKING

from error_logger import log_exception

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


class InteractionNavigationMixin:
    """Клик-обработчики, зум, панорамирование, колесо мыши."""

    def _pick_rack_from_cursor(self: "ModelViewWidget") -> tuple[str | None, str]:
        """Определить id стеллажа под курсором и контекстную зону."""
        zone_id = str(getattr(self, "_rack_preview_zone_id", "") or "")
        if not zone_id and getattr(self, "_rack_entries", None):
            current_selected = str(getattr(self, "_selected_rack_id", "") or "")
            if current_selected:
                for entry in list(getattr(self, "_rack_entries", []) or []):
                    if str(entry.get("rack_id", "")) == current_selected:
                        zone_id = str(entry.get("zone_id", "") or "")
                        break
            if not zone_id:
                first_entry = next(iter(list(getattr(self, "_rack_entries", []) or [])), None)
                if first_entry:
                    zone_id = str(first_entry.get("zone_id", "") or "")

        rack_id = None
        if self._plotter and self._plotter.interactor and hasattr(self, "_find_rack_id_by_screen"):
            try:
                sx, sy = self._plotter.interactor.GetEventPosition()
                rack_id = self._find_rack_id_by_screen(float(sx), float(sy), zone_id, screen_is_vtk=True)
            except Exception as e:
                log_exception(__name__, "_pick_rack_from_cursor", e)
                rack_id = None
        return rack_id, zone_id

    def _pick_zone_from_cursor(
        self: "ModelViewWidget",
        x: float | None,
        y: float | None,
        z: float | None,
    ) -> str | None:
        """Определить id зоны под текущим положением курсора."""
        if not self._plotter:
            return None

        zone_id = None
        try:
            from vtkmodules.vtkRenderingCore import vtkPropPicker

            sx, sy = self._plotter.interactor.GetEventPosition()
            prop_picker = vtkPropPicker()
            prop_picker.Pick(sx, sy, 0, self._plotter.renderer)
            picked_actor = prop_picker.GetViewProp()
            if picked_actor is not None:
                zone_id = self._find_zone_by_actor(picked_actor)
        except Exception as e:
            log_exception(__name__, "_pick_zone_from_cursor", e)
            zone_id = None

        if not zone_id:
            try:
                sx, sy = self._plotter.interactor.GetEventPosition()
                zone_id = self._find_zone_by_screen_ray(sx, sy)
            except Exception as e:
                log_exception(__name__, "_pick_zone_from_cursor", e)
                zone_id = None

        if not zone_id:
            try:
                sx, sy = self._plotter.interactor.GetEventPosition()
                plane_z = self._get_grid_plane_z(getattr(self, "_grid_origin", None))
                world = self.screen_to_world_on_plane(sx, sy, plane_z)
                if world is not None:
                    best_top_z = None
                    for zid, polygon in self._zone_polygons.items():
                        if not polygon or len(polygon) < 3:
                            continue
                        inside, _ = self._classify_point_in_polygon(
                            world[0], world[1], polygon,
                        )
                        if inside:
                            top_z = self._get_zone_top_height(zid)
                            if best_top_z is None or top_z > best_top_z:
                                best_top_z = top_z
                                zone_id = zid
            except Exception as e:
                log_exception(__name__, "_pick_zone_from_cursor", e)

        if not zone_id:
            zone_id = self._find_zone_at_point(x, y, z)
        return zone_id

    def _on_plotter_click(self: "ModelViewWidget", point):
        """Обработчик клика на 3D-сцене."""
        if not self._models_loaded:
            return

        if self._ignore_next_plotter_click:
            self._ignore_next_plotter_click = False
            return

        x = y = z = None
        if point is not None:
            try:
                x, y, z = point
            except Exception as e:
                log_exception(__name__, "_on_plotter_click", e)
                x = y = z = None

        if self._custom_click_handler:
            try:
                if self._custom_click_handler(x, y, z):
                    return
            except Exception as e:
                log_exception(__name__, "_on_plotter_click", e)
                return

        # Выбор полки в изолированном стеллаже (rack-level), когда выбор зон отключён.
        # Полка кликабельна только при активной rack-изоляции (последовательная навигация).
        if not self._zone_pick_enabled and not self._zone_selection_mode:
            try:
                _iso_active = bool(getattr(self, "_rack_isolation_active", False))
                rid = str(getattr(self, "_rack_isolation_rack_id", "") or "") if _iso_active else ""
                if rid and self._plotter and self._plotter.interactor and hasattr(self, "_find_rendered_shelf_by_screen"):
                    sx, sy = self._plotter.interactor.GetEventPosition()
                    shelf_hit = self._find_rendered_shelf_by_screen(float(sx), float(sy), rid, screen_is_vtk=True)
                    if shelf_hit:
                        slot_id, shelf_index = shelf_hit
                        tree_shelf_index = int(max(1, int(shelf_index)))
                        try:
                            entry = self._get_rack_entry(rid) if hasattr(self, "_get_rack_entry") else None
                            rack_type = str(((entry or {}).get("params") or {}).get("rack_type") or "").strip().upper()
                            if rack_type == "PALLET":
                                tree_shelf_index = int(max(1, int(shelf_index)) + 1)
                        except Exception as e:
                            log_exception(__name__, "_on_plotter_click", e)
                            tree_shelf_index = int(max(1, int(shelf_index)))
                        self._selected_shelf_visual_rack_id = rid
                        self._selected_shelf_visual_slot_id = str(slot_id)
                        self._selected_shelf_visual_index = int(tree_shelf_index)
                        if hasattr(self, "_highlight_selected_rendered_shelf"):
                            self._highlight_selected_rendered_shelf(
                                rid,
                                str(slot_id),
                                int(max(1, int(shelf_index))),
                            )
                        self.shelf_slot_selected.emit(rid, str(slot_id))  # type: ignore[attr-defined]
                        return
                    # Для PALLET разрешаем активацию уровня "под первой полкой"
                    # кликом по области слота (даже если slot_actor скрыт в изоляции).
                    try:
                        entry = self._get_rack_entry(rid) if hasattr(self, "_get_rack_entry") else None
                        zone_id = str((entry or {}).get("zone_id") or "")
                        slot_hit_id = self._find_slot_id_by_screen(
                            float(sx),
                            float(sy),
                            rid,
                            zone_id,
                            screen_is_vtk=True,
                            include_hidden=True,
                        ) if hasattr(self, "_find_slot_id_by_screen") else None
                        rack_type = str(((entry or {}).get("params") or {}).get("rack_type") or "").strip().upper()
                        if slot_hit_id and rack_type == "PALLET":
                            self._selected_shelf_visual_rack_id = rid
                            self._selected_shelf_visual_slot_id = str(slot_hit_id)
                            self._selected_shelf_visual_index = 1
                            if hasattr(self, "_highlight_floor_shelf_polygon"):
                                self._highlight_floor_shelf_polygon(rid, str(slot_hit_id))
                            self.shelf_slot_selected.emit(rid, str(slot_hit_id))  # type: ignore[attr-defined]
                            return
                    except Exception as e:
                        log_exception(__name__, "_on_plotter_click", e)
            except Exception as e:
                log_exception(__name__, "_on_plotter_click", e)

        if self._zone_pick_enabled and not self._zone_selection_mode and x is not None:
            zone_id = self._pick_zone_from_cursor(x, y, z)
            if zone_id:
                self.zone_selected.emit(zone_id)
                return

        # В режиме сетки/контура создание точек не зависит от фиксации камеры.
        if self._zone_selection_mode:
            x, y, z = self._resolve_grid_click_point(x, y, z, action="add")
            if x is None:
                return
            z = float(z) if z is not None else 0.0
            self._handle_grid_click(x, y, z, action="add")
            return

    def _on_plotter_double_click(self: "ModelViewWidget", point):
        """Обработчик двойного ЛКМ: выбрать и сфокусировать зону."""
        if not self._models_loaded or self._zone_selection_mode:
            return

        # Двойной ЛКМ по полке в изолированном стеллаже: синхронизировать дерево и сфокусировать полку.
        # Полка доступна только при активной rack-изоляции (последовательная навигация zone→rack→shelf).
        try:
            _iso_active = bool(getattr(self, "_rack_isolation_active", False))
            rid = str(getattr(self, "_rack_isolation_rack_id", "") or "") if _iso_active else ""
            if rid and self._plotter and self._plotter.interactor and hasattr(self, "_find_rendered_shelf_by_screen"):
                sx, sy = self._plotter.interactor.GetEventPosition()
                shelf_hit = self._find_rendered_shelf_by_screen(float(sx), float(sy), rid, screen_is_vtk=True)
                if shelf_hit:
                    slot_id, shelf_index = shelf_hit
                    tree_shelf_index = int(max(1, int(shelf_index)))
                    try:
                        entry = self._get_rack_entry(rid) if hasattr(self, "_get_rack_entry") else None
                        rack_type = str(((entry or {}).get("params") or {}).get("rack_type") or "").strip().upper()
                        if rack_type == "PALLET":
                            tree_shelf_index = int(max(1, int(shelf_index)) + 1)
                    except Exception as e:
                        log_exception(__name__, "_on_plotter_double_click", e)
                        tree_shelf_index = int(max(1, int(shelf_index)))
                    self._selected_shelf_visual_rack_id = rid
                    self._selected_shelf_visual_slot_id = str(slot_id)
                    self._selected_shelf_visual_index = int(tree_shelf_index)
                    if hasattr(self, "_highlight_selected_rendered_shelf"):
                        self._highlight_selected_rendered_shelf(
                            rid,
                            str(slot_id),
                            int(max(1, int(shelf_index))),
                        )
                    self.shelf_slot_selected.emit(rid, str(slot_id))  # type: ignore[attr-defined]
                    try:
                        self.shelf_slot_double_clicked.emit(
                            rid,
                            str(slot_id),
                            int(tree_shelf_index),
                        )  # type: ignore[attr-defined]
                    except Exception as e:
                        log_exception(__name__, "_on_plotter_double_click", e)
                    zone_id = ""
                    if hasattr(self, "_get_rack_entry"):
                        entry = self._get_rack_entry(rid)
                        if entry:
                            zone_id = str(entry.get("zone_id") or "")
                    if hasattr(self, "focus_on_shelf_slot_isometric"):
                        self.focus_on_shelf_slot_isometric(rid, str(slot_id), int(tree_shelf_index), zone_id)
                    return
                # Для PALLET: двойной клик по области слота активирует уровень пола (index=0).
                entry = self._get_rack_entry(rid) if hasattr(self, "_get_rack_entry") else None
                zone_id = str((entry or {}).get("zone_id") or "")
                slot_hit_id = self._find_slot_id_by_screen(
                    float(sx),
                    float(sy),
                    rid,
                    zone_id,
                    screen_is_vtk=True,
                    include_hidden=True,
                ) if hasattr(self, "_find_slot_id_by_screen") else None
                rack_type = str(((entry or {}).get("params") or {}).get("rack_type") or "").strip().upper()
                if slot_hit_id and rack_type == "PALLET":
                    self._selected_shelf_visual_rack_id = rid
                    self._selected_shelf_visual_slot_id = str(slot_hit_id)
                    self._selected_shelf_visual_index = 1
                    if hasattr(self, "_highlight_floor_shelf_polygon"):
                        self._highlight_floor_shelf_polygon(rid, str(slot_hit_id))
                    self.shelf_slot_selected.emit(rid, str(slot_hit_id))  # type: ignore[attr-defined]
                    try:
                        self.shelf_slot_double_clicked.emit(
                            rid,
                            str(slot_hit_id),
                            1,
                        )  # type: ignore[attr-defined]
                    except Exception as e:
                        log_exception(__name__, "_on_plotter_double_click", e)
                    if hasattr(self, "focus_on_shelf_slot_isometric"):
                        self.focus_on_shelf_slot_isometric(rid, str(slot_hit_id), 1, zone_id)
                    return
        except Exception as e:
            log_exception(__name__, "_on_plotter_double_click", e)

        # При двойном клике по стеллажу — выделение и фокус камеры на стеллаж.
        rack_id, rack_zone_id = self._pick_rack_from_cursor()
        if rack_id:
            if hasattr(self, "_set_selected_rack"):
                try:
                    self._set_selected_rack(rack_id)
                except Exception as e:
                    log_exception(__name__, "_on_plotter_double_click", e)
            try:
                self.rack_double_clicked.emit(str(rack_id), str(rack_zone_id or ""))
            except Exception as e:
                log_exception(__name__, "_on_plotter_double_click", e)
                if hasattr(self, "focus_on_rack_isometric"):
                    self.focus_on_rack_isometric(rack_id, rack_zone_id)
            return

        if not self._zone_pick_enabled:
            return

        x = y = z = None
        if point is not None:
            try:
                x, y, z = point
            except Exception as e:
                log_exception(__name__, "_on_plotter_double_click", e)
                x = y = z = None

        zone_id = self._pick_zone_from_cursor(x, y, z)
        if not zone_id:
            return
        self.zone_selected.emit(zone_id)
        try:
            self.zone_double_clicked.emit(zone_id)
        except Exception as e:
            log_exception(__name__, "_on_plotter_double_click", e)
        if hasattr(self, "focus_on_zone_isometric"):
            self.focus_on_zone_isometric(zone_id)

    def _on_plotter_right_click(self: "ModelViewWidget", point):
        """Обработчик ПКМ на 3D-сцене (используется для сокращения объёма сетки)."""
        if not self._models_loaded:
            return
        if not self._zone_selection_mode:
            return
        # В contour_edit ПКМ обрабатывается через сценарий eventFilter,
        # чтобы гарантированно блокировать перехват камеры.
        if self.is_scenario_active("contour_edit"):
            return
        x = y = z = None
        if point is not None:
            try:
                x, y, z = point
            except Exception as e:
                log_exception(__name__, "_on_plotter_right_click", e)
                x = y = z = None
        x, y, z = self._resolve_grid_click_point(x, y, z, action="remove")
        if x is None:
            return
        z = float(z) if z is not None else 0.0
        self._handle_grid_click(x, y, z, action="remove")

    # -- масштабирование ------------------------------------------------------

    def zoom_in(self: "ModelViewWidget"):
        """Увеличение масштаба."""
        if not self._models_loaded or not self._plotter:
            return
        self._plotter.camera.zoom(1.2)
        if hasattr(self, "_reset_camera_clipping_range"):
            self._reset_camera_clipping_range()
        self._plotter.update()

    def zoom_out(self: "ModelViewWidget"):
        """Уменьшение масштаба."""
        if not self._models_loaded or not self._plotter:
            return
        self._plotter.camera.zoom(0.8)
        if hasattr(self, "_reset_camera_clipping_range"):
            self._reset_camera_clipping_range()
        self._plotter.update()

    def _pan_camera_by_pixels(self: "ModelViewWidget", dx: float, dy: float) -> None:
        """Сместить камеру в плоскости экрана на величину в пикселях."""
        if not self._plotter or not getattr(self._plotter, "camera", None):
            return
        try:
            cam = self._plotter.camera
            pos = cam.GetPosition()
            foc = cam.GetFocalPoint()
            up = cam.GetViewUp()

            vx = foc[0] - pos[0]
            vy = foc[1] - pos[1]
            vz = foc[2] - pos[2]
            dist = math.sqrt(vx * vx + vy * vy + vz * vz) or 1.0

            # right = normalize(направление_взгляда × up)
            rx = vy * up[2] - vz * up[1]
            ry = vz * up[0] - vx * up[2]
            rz = vx * up[1] - vy * up[0]
            rlen = math.sqrt(rx * rx + ry * ry + rz * rz) or 1.0
            rx, ry, rz = rx / rlen, ry / rlen, rz / rlen

            # нормализация вектора up
            ulen = math.sqrt(up[0] * up[0] + up[1] * up[1] + up[2] * up[2]) or 1.0
            ux, uy, uz = up[0] / ulen, up[1] / ulen, up[2] / ulen

            # масштаб переноса: пропорционален удалению камеры
            scale = dist / 700.0
            tx = (-dx * rx + dy * ux) * scale
            ty = (-dx * ry + dy * uy) * scale
            tz = (-dx * rz + dy * uz) * scale

            cam.SetPosition(pos[0] + tx, pos[1] + ty, pos[2] + tz)
            cam.SetFocalPoint(foc[0] + tx, foc[1] + ty, foc[2] + tz)
        except Exception as e:
            log_exception(__name__, "_pan_camera_by_pixels", e)
            return

    def _safe_render(self: "ModelViewWidget", min_interval_s: float = 1.0 / 60.0) -> None:
        """Безопасный обновляющий рендер с ограничением частоты."""
        plotter = self._plotter
        if not plotter or not self._models_loaded:
            return
        try:
            interactor = getattr(plotter, "interactor", None)
            if interactor is None:
                return
            if hasattr(plotter, "isVisible") and not plotter.isVisible():
                return
            if hasattr(interactor, "isVisible") and not interactor.isVisible():
                return
            now = time.monotonic()
            last_ts = float(getattr(self, "_last_safe_render_ts", 0.0))
            if now - last_ts < float(min_interval_s):
                return
            self._last_safe_render_ts = now
            if hasattr(plotter, "update"):
                plotter.update()
            else:
                plotter.render()
        except Exception as e:
            log_exception(__name__, "_safe_render", e)
            return

    # -- Qt события -----------------------------------------------------------

    def wheelEvent(self: "ModelViewWidget", event):
        """Обработка колесика мыши для зума."""
        if self._camera_locked:
            event.accept()
            return
        if self._models_loaded and self._plotter:
            delta = event.angleDelta().y()
            if delta > 0:
                self.zoom_in()
            else:
                self.zoom_out()
        event.accept()


# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
#    Навигация камеры: клики, масштаб, панорамирование, безопасный рендер.
#
# 2) Последовательность действий и вызовов:
#    A. Класс InteractionNavigationMixin: точки входа
#       Публичные методы сценария:
#       - InteractionNavigationMixin.zoom_in(...)
#       - InteractionNavigationMixin.zoom_out(...)
#       - InteractionNavigationMixin.wheelEvent(...)
#
#    B. InteractionNavigationMixin: основной сценарий:
#       InteractionNavigationMixin._on_plotter_click(...)
#         Назначение: Обработчик клика на 3D-сцене.
#       InteractionNavigationMixin._on_plotter_right_click(...)
#         Назначение: Обработчик ПКМ на 3D-сцене (используется для сокращения объёма сетки).
#       InteractionNavigationMixin.zoom_in(...)
#         Назначение: Увеличение масштаба.
#       InteractionNavigationMixin.zoom_out(...)
#         Назначение: Уменьшение масштаба.
#       InteractionNavigationMixin._pan_camera_by_pixels(...)
#         Назначение: Сместить камеру в плоскости экрана на величину в пикселях.
#       InteractionNavigationMixin.wheelEvent(...)
#         Назначение: Обработка колесика мыши для зума.
#         Последовательность внутренних вызовов:
#         -> InteractionNavigationMixin.zoom_in(...)
#         -> InteractionNavigationMixin.zoom_out(...)
#
#    C. InteractionNavigationMixin: вспомогательные расчёты:
#       InteractionNavigationMixin._safe_render(...)
#         Назначение: Безопасный обновляющий рендер с ограничением частоты.
#
# 3) Важные ограничения и инварианты:
#    - Модуль выполняется в составе ModelViewWidget и использует согласованные поля состояния self._... .
#    - Все операции с актёрами и камерой выполняются только при валидном self._plotter с безопасной обработкой исключений.
#    - Межмодульная связность: только через фасад model_view; прямые обращения между zone, rack, shelf, cell запрещены.
#    - Очистка состояния должна быть идемпотентной: повторный вызов не меняет корректное состояние в ошибочное.
#
# 4) Правило сопровождения:
#    - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
#    - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).
