# -*- coding: utf-8 -*-
# gui/components/model_view/_mv_interaction_core.py
# Базовые помощники взаимодействия: преобразование координат, настройка камеры, разрешение сетки

from __future__ import annotations

from typing import Optional, TYPE_CHECKING

from error_logger import log_exception

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


class InteractionCoreMixin:
    """Базовые вспомогательные методы взаимодействия: координаты, камера, сетка, утилиты interactor."""

    # -- Утилиты interactor (из бывшего InteractionUtilsMixin) -----------------

    def _is_plotter_interactor_event(self: "ModelViewWidget", watched) -> bool:
        """Проверить, что событие пришло от активного interactor plotter."""
        return bool(self._plotter and watched is self._plotter.interactor)

    def _event_to_vtk_display_xy(self: "ModelViewWidget", event) -> tuple[int, int]:
        """Перевести координаты события Qt в VTK display (с DPR и инверсией Y)."""
        dpr = self._plotter.devicePixelRatio()
        sx = int(round(event.position().x() * dpr))
        sy = int(round(event.position().y() * dpr))
        rw = self._plotter.ren_win
        _, vh = rw.GetSize() if rw else (0, 0)
        sy = vh - sy - 1
        return sx, sy

    # -- Координатные преобразования ------------------------------------------

    def _event_world_on_plane(self: "ModelViewWidget", event, plane_z: float) -> Optional[tuple[float, float, float]]:
        """Преобразование события мыши интерактора в мировые координаты на фиксированной плоскости."""
        if not self._plotter:
            return None
        try:
            sx, sy = self._event_to_vtk_display_xy(event)
            return self.screen_to_world_on_plane(sx, sy, plane_z)
        except Exception as e:
            log_exception(__name__, "_event_world_on_plane", e)
            return None

    def _update_contour_drag_point(self: "ModelViewWidget", world_x: float, world_y: float) -> bool:
        """Перемещение перетаскиваемой точки контура к ближайшему узлу сетки без изменения порядка."""
        idx = getattr(self, "_contour_drag_point_index", None)
        if idx is None:
            return False
        points = list(getattr(self, "_contour_points", []) or [])
        if idx < 0 or idx >= len(points):
            return False

        node = self._nearest_grid_node(float(world_x), float(world_y))
        if node is None:
            return False
        new_node = (float(node[0]), float(node[1]))
        current_node = (float(points[idx][0]), float(points[idx][1]))
        if new_node == current_node:
            return False

        # Не допускаем совпадения с другими узлами.
        for i, pt in enumerate(points):
            if i == idx:
                continue
            if (float(pt[0]), float(pt[1])) == new_node:
                return False

        points[idx] = new_node
        self._contour_points = points

        if getattr(self, "_dim_enabled", False):
            self.set_dim_last_point(self._contour_points[-1] if self._contour_points else None)
        self._update_contour_visualization()
        self._update_selected_cells_from_contour()
        emit_ready = getattr(self, "_emit_contour_ready_state", None)
        if callable(emit_ready):
            emit_ready()
        return True

    # -- камера ---------------------------------------------------------------

    def _apply_facility_isometric_view(
        self: "ModelViewWidget",
        reset_camera: bool = True,
        update: bool = True,
    ) -> None:
        """Применить единый изометрический вид facility (базовая ориентация)."""
        if not self._plotter:
            return
        try:
            self._plotter.view_isometric()
            camera = getattr(self._plotter, "camera", None)
            if camera is not None and hasattr(camera, "Azimuth"):
                # Базовая ориентация facility: изометрия с противоположной стороны.
                camera.Azimuth(180)
                if hasattr(camera, "OrthogonalizeViewUp"):
                    camera.OrthogonalizeViewUp()
            if reset_camera:
                self._plotter.reset_camera()
                # Поджимаем общий facility-кадр: небольшие поля вокруг модели.
                if camera is not None and hasattr(camera, "zoom"):
                    try:
                        camera.zoom(1.35)
                    except Exception as e:
                        log_exception(__name__, "_apply_facility_isometric_view.camera_zoom", e)
            if hasattr(self, "_reset_camera_clipping_range"):
                self._reset_camera_clipping_range()
            if update:
                self._plotter.update()
        except Exception as e:
            log_exception(__name__, "_apply_facility_isometric_view", e)

    def _setup_trackball_right_button(self: "ModelViewWidget"):
        """Настройка trackball стиля: правая кнопка — поворот, левая отключена."""
        try:
            if self._plotter and self._plotter.interactor:
                interactor = self._plotter.interactor
                style = (
                    interactor.GetInteractorStyle()
                    if hasattr(interactor, "GetInteractorStyle")
                    else None
                )
                if style:
                    if hasattr(style, "SetLeftButtonMotion"):
                        style.SetLeftButtonMotion(False)
                    if hasattr(style, "SetRightButtonMotion"):
                        style.SetRightButtonMotion(True)
        except Exception as e:
            log_exception(__name__, "_setup_trackball_right_button", e)

    def _set_trackball_right_button_enabled(self: "ModelViewWidget", enabled: bool) -> None:
        """Явно включить/выключить реакцию VTK-style на ПКМ."""
        try:
            if not (self._plotter and self._plotter.interactor):
                return
            interactor = self._plotter.interactor
            style = (
                interactor.GetInteractorStyle()
                if hasattr(interactor, "GetInteractorStyle")
                else None
            )
            if style and hasattr(style, "SetRightButtonMotion"):
                style.SetRightButtonMotion(bool(enabled))
        except Exception as e:
            log_exception(__name__, "_set_trackball_right_button_enabled", e)

    # -- клик -----------------------------------------------------------------

    def set_top_view_navigation(self: "ModelViewWidget", enabled: bool) -> None:
        """Режим top-view: pan/zoom без вращения камеры."""
        if not self._plotter:
            return
        try:
            if enabled:
                try:
                    self._plotter.view_xy()
                except Exception as e:
                    log_exception(__name__, "set_top_view_navigation.view_xy", e)
                    self._plotter.camera_position = "xy"
                self._plotter.enable_image_style()
            else:
                self._plotter.enable_trackball_style()
                self._setup_trackball_right_button()
            self._plotter.update()
        except Exception as e:
            log_exception(__name__, "set_top_view_navigation", e)

    def set_isometric_view(self: "ModelViewWidget") -> None:
        """Переключить камеру в изометрический вид."""
        self._apply_facility_isometric_view(reset_camera=True, update=True)

    def animate_facility_isometric_view(
        self: "ModelViewWidget",
        *,
        duration_ms: int = 360,
        steps: int = 18,
    ) -> None:
        """Плавно перевести камеру к общей изометрии facility."""
        if not self._plotter:
            return
        cam = getattr(self._plotter, "camera", None)
        if cam is None:
            return

        try:
            start_pos = tuple(float(v) for v in cam.GetPosition())
            start_focal = tuple(float(v) for v in cam.GetFocalPoint())
            start_up = tuple(float(v) for v in cam.GetViewUp())
        except Exception as e:
            log_exception(__name__, "animate_facility_isometric_view.get_camera_start", e)
            self.set_isometric_view()
            return

        # Рассчитываем целевую изометрию без промежуточного переключения
        # живой камеры (иначе возникает визуальный "проскок" статичного кадра).
        try:
            bounds = getattr(self, "_room_bounds", None)
            if not bounds:
                zone_bounds = list((getattr(self, "_zone_data", {}) or {}).values())
                if zone_bounds:
                    min_x = min(float(b[0]) for b in zone_bounds)
                    max_x = max(float(b[1]) for b in zone_bounds)
                    min_y = min(float(b[2]) for b in zone_bounds)
                    max_y = max(float(b[3]) for b in zone_bounds)
                    min_z = min(float(b[4]) for b in zone_bounds)
                    max_z = max(float(b[5]) for b in zone_bounds)
                    bounds = (min_x, max_x, min_y, max_y, min_z, max_z)
            if not bounds:
                self.set_isometric_view()
                return

            min_x, max_x, min_y, max_y, min_z, max_z = [float(v) for v in bounds]
            cx = (min_x + max_x) * 0.5
            cy = (min_y + max_y) * 0.5
            sx = max(1.0, max_x - min_x)
            sy = max(1.0, max_y - min_y)
            sz = max(1.0, max_z - min_z)
            span = max(sx, sy, sz)
            cz = min_z + sz * 0.5

            vx, vy, vz = 1.0, -1.0, 0.75
            norm = (vx * vx + vy * vy + vz * vz) ** 0.5
            vx, vy, vz = vx / norm, vy / norm, vz / norm

            ux, uy, uz = 0.0, 0.0, 1.0
            fx, fy, fz = -vx, -vy, -vz
            rx = fy * uz - fz * uy
            ry = fz * ux - fx * uz
            rz = fx * uy - fy * ux
            r_norm = (rx * rx + ry * ry + rz * rz) ** 0.5
            if r_norm <= 1e-6:
                rx, ry, rz = 1.0, 0.0, 0.0
            else:
                rx, ry, rz = rx / r_norm, ry / r_norm, rz / r_norm

            facility_margin = max(1.01, float(getattr(self, "_facility_iso_margin_factor", 1.16)))
            facility_min_distance = max(1000.0, float(getattr(self, "_facility_iso_min_distance", 2280.0)))
            facility_side_shift = max(0.0, float(getattr(self, "_facility_iso_side_shift_factor", 0.06)))
            facility_z_lift = max(0.0, float(getattr(self, "_facility_iso_z_lift_factor", 0.10)))
            if hasattr(self, "_compute_isometric_fit_distance"):
                distance = float(
                    self._compute_isometric_fit_distance(
                        (min_x, max_x, min_y, max_y, min_z, max_z),
                        view_dir=(vx, vy, vz),
                        up_dir=(ux, uy, uz),
                        margin_factor=facility_margin,
                        min_distance=facility_min_distance,
                    )
                )
            else:
                distance = max(facility_min_distance, span * 1.68)
            target_focal = (cx, cy, cz)
            target_pos = (
                cx + vx * distance + rx * (span * facility_side_shift),
                cy + vy * distance + ry * (span * facility_side_shift),
                cz + vz * distance + (sz * facility_z_lift),
            )
            target_up = (ux, uy, uz)
        except Exception as e:
            log_exception(__name__, "animate_facility_isometric_view.compute_target", e)
            self.set_isometric_view()
            return

        if hasattr(self, "_animate_camera_transition"):
            try:
                self._animate_camera_transition(
                    start_pos=start_pos,
                    start_focal=start_focal,
                    start_up=start_up,
                    target_pos=target_pos,
                    target_focal=target_focal,
                    target_up=target_up,
                    duration_ms=int(duration_ms),
                    steps=int(steps),
                )
                return
            except Exception as e:
                log_exception(__name__, "animate_facility_isometric_view.animate_transition", e)

        self.set_isometric_view()

    def _resolve_grid_click_point(
        self: "ModelViewWidget",
        x: Optional[float],
        y: Optional[float],
        z: Optional[float],
        action: str,
    ) -> tuple[Optional[float], Optional[float], Optional[float]]:
        """Выбрать наилучшую world-точку для клика по сетке.

        Приоритет кандидатов:
        1. Прямой расчёт из позиции Qt-курсора с DPR-коррекцией
           (независимо от VTK event pipeline).
        2. screen_to_world_on_plane через VTK event position
           (проекция на Z-плоскость сетки, без зависимости от геометрии).
        3. pick_world (VTK picker — может попасть по чужой геометрии).
        4. Исходная точка из PyVista callback.
        """
        if not self._plotter or not self._plotter.interactor:
            return x, y, z

        candidates: list[tuple[Optional[float], Optional[float], Optional[float]]] = []
        if self.is_scenario_active("contour_edit"):
            plane_z = self._get_contour_plane_z()
        else:
            plane_z = self._get_grid_plane_z(getattr(self, "_grid_origin", None))

        # ── 1. Кандидат из позиции Qt-курсора (DPR-safe) ─────────────
        try:
            from PySide6.QtGui import QCursor
            global_pos = QCursor.pos()
            local_pos = self._plotter.mapFromGlobal(global_pos)
            dpr = self._plotter.devicePixelRatio()
            ren_win = self._plotter.ren_win
            if ren_win:
                _, win_h = ren_win.GetSize()
                q_sx = int(round(local_pos.x() * dpr))
                q_sy = win_h - int(round(local_pos.y() * dpr)) - 1
                wp = self.screen_to_world_on_plane(q_sx, q_sy, plane_z)
                if wp is not None:
                    candidates.append((wp[0], wp[1], wp[2]))
        except Exception as e:
            log_exception(__name__, "_resolve_grid_click_point.qt_cursor_candidate", e)

        # ── 2/3. Кандидаты из VTK event position ─────────────────────
        try:
            sx, sy = self._plotter.interactor.GetEventPosition()
        except Exception as e:
            log_exception(__name__, "_resolve_grid_click_point.get_event_position", e)
            sx = sy = None

        if sx is not None and sy is not None:
            try:
                world = self.screen_to_world_on_plane(sx, sy, plane_z)
                if world is not None:
                    candidates.append((world[0], world[1], world[2]))
            except Exception as e:
                log_exception(__name__, "_resolve_grid_click_point.screen_to_world", e)
            try:
                picked = self.pick_world(sx, sy)
                if picked is not None:
                    candidates.append((picked[0], picked[1], picked[2]))
            except Exception as e:
                log_exception(__name__, "_resolve_grid_click_point.pick_world", e)

        # ── 4. Исходная точка из PyVista ──────────────────────────────
        candidates.append((x, y, z))

        # Для ПКМ приоритет у точки, попадающей в уже выделенную ячейку.
        prefer_selected = (action == "remove")
        for cx, cy, cz in candidates:
            if cx is None or cy is None:
                continue
            found = None
            try:
                found = self._find_cell_by_point(float(cx), float(cy), prefer_selected=prefer_selected)
            except Exception as e:
                log_exception(__name__, "_resolve_grid_click_point.find_cell_by_point", e)
                found = None
            if found is not None:
                return float(cx), float(cy), 0.0

        # fallback: первая валидная точка
        for cx, cy, cz in candidates:
            if cx is not None and cy is not None:
                return float(cx), float(cy), 0.0
        return x, y, z

    def _is_point_inside_room(self: "ModelViewWidget", x: float, y: float, z: float) -> bool:
        """Проверка, находится ли точка внутри помещения."""
        if not self._room_bounds:
            return True
        mn_x, mx_x, mn_y, mx_y, mn_z, mx_z = self._room_bounds
        margin = 10
        return (
            mn_x - margin <= x <= mx_x + margin
            and mn_y - margin <= y <= mx_y + margin
            and mn_z - margin <= z <= mx_z + margin
        )


# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
#    Базовые операции взаимодействия: координатные преобразования, режимы камеры, проверка точек.
#
# 2) Последовательность действий и вызовов:
#    A. Класс InteractionCoreMixin: утилиты interactor + координаты + камера + сетка.
#       - _is_plotter_interactor_event() — проверка источника события
#       - _event_to_vtk_display_xy()    — Qt logical → VTK display
#       - _event_world_on_plane()       — мировые координаты на плоскости
#       - set_camera_locked()           — блокировка/разблокировка камеры
#       - set_top_view_navigation()     — top-view: pan/zoom без вращения
#       - set_isometric_view()          — изометрический вид
#       - _update_contour_drag_point()  — перемещение узла контура
#       - _resolve_grid_click_point()   — world-точка клика по сетке
#       - _is_point_inside_room()       — внутри ли помещения
#
# 3) Важные ограничения и инварианты:
#    - Выполняется в составе ModelViewWidget с согласованными полями self._... .
#    - Операции с актёрами/камерой только при валидном self._plotter.
#    - Очистка состояния идемпотентна.
