# -*- coding: utf-8 -*-
# gui/components/simple_model_viewer/model_view_widget.py
"""Автономный простой 3D-просмотрщик моделей, используемый PartVisualizer."""

import math
from pathlib import Path

from PySide6.QtCore import Qt, Signal, QEvent
from PySide6.QtWidgets import QWidget

from gui.containers import VContainer
from gui.styles import APP_STYLES
from error_logger import log_exception

try:
    import pyvista as pv
    from pyvistaqt import QtInteractor
    PYVISTA_AVAILABLE = True
except ImportError:
    PYVISTA_AVAILABLE = False
    print("Warning: PyVista is not installed. 3D preview is unavailable.")



class ModelViewWidget(QWidget):
    """Компактный виджет предпросмотра STL с ограниченным управлением мышью."""

    error_occurred = Signal(str)

    def __init__(self, *_, **__):
        super().__init__()
        self._plotter = None
        self._model_loaded = False
        self._cam_rotate_active = False
        self._cam_pan_active = False
        self._cam_last_pos: tuple[float, float] | None = None
        self._setup_ui()

    def _setup_ui(self) -> None:
        main_container = VContainer()
        main_container.layout().setAlignment(Qt.AlignmentFlag.AlignCenter)
        self._model_layout = main_container.layout()
        self.setLayout(main_container.layout())
        self.setStyleSheet(APP_STYLES.get("VIEW_WIDGET_BACKGROUND", ""))

    def load_model(self, model_path: str | Path) -> None:
        if not PYVISTA_AVAILABLE:
            self._show_error("PyVista is not installed")
            return

        path = Path(model_path)
        if not path.exists():
            self._show_error(f"Model file not found: {path}")
            return

        self.clear_model()

        try:
            mesh = pv.read(path)
            self._plotter = QtInteractor(self)
            self._plotter.set_background((240 / 255, 240 / 255, 240 / 255))
            self._plotter.add_mesh(mesh, color="lightblue", show_edges=True)
            self._plotter.camera_position = "xy"
            self._plotter.reset_camera()
            self._configure_mouse_controls()
            self._model_layout.addWidget(self._plotter)
            self._model_loaded = True
        except Exception as exc:
            self._show_error(f"Model load error: {exc}")

    def clear_model(self) -> None:
        if self._plotter is not None:
            try:
                self._plotter.close()
            except Exception as e:
                log_exception(__name__, "clear_model.close_plotter", e)
            self._plotter = None

        while self._model_layout.count():
            item = self._model_layout.takeAt(0)
            widget = item.widget()
            if widget is not None:
                widget.setParent(None)
                widget.deleteLater()

        self._model_loaded = False

    def set_isometric_view(self) -> None:
        if not self._model_loaded or not self._plotter:
            return
        self._plotter.view_isometric()
        self._plotter.update()

    def set_top_view(self) -> None:
        if not self._model_loaded or not self._plotter:
            return
        self._plotter.view_xy()
        self._plotter.update()

    def set_front_view(self) -> None:
        if not self._model_loaded or not self._plotter:
            return
        self._plotter.view_xz()
        self._plotter.update()

    def set_left_view(self) -> None:
        if not self._model_loaded or not self._plotter:
            return
        self._plotter.view_zy()
        self._plotter.update()

    def set_right_view(self) -> None:
        if not self._model_loaded or not self._plotter:
            return
        self._plotter.view_yz()
        self._plotter.update()

    def reset_view(self) -> None:
        if not self._model_loaded or not self._plotter:
            return
        self._plotter.reset_camera()
        self._plotter.camera_position = "xy"
        self._plotter.update()

    def zoom_in(self) -> None:
        if not self._model_loaded or not self._plotter:
            return
        self._plotter.camera.zoom(1.2)
        self._plotter.update()

    def zoom_out(self) -> None:
        if not self._model_loaded or not self._plotter:
            return
        self._plotter.camera.zoom(0.8)
        self._plotter.update()

    def wheelEvent(self, event) -> None:
        if self._model_loaded and self._plotter:
            delta = event.angleDelta().y()
            if delta > 0:
                self.zoom_in()
            else:
                self.zoom_out()
        event.accept()

    def resizeEvent(self, event) -> None:
        super().resizeEvent(event)
        if self._model_loaded and self._plotter:
            self._plotter.update()

    def _show_error(self, message: str) -> None:
        self.clear_model()
        self.error_occurred.emit(message)

    def _configure_mouse_controls(self) -> None:
        """ПКМ — вращение, колесо — масштаб, СКМ — панорамирование, ЛКМ — отключена."""
        if not self._plotter:
            return
        try:
            self._plotter.enable_trackball_style()
        except Exception as e:
            log_exception(__name__, "_configure_mouse_controls.enable_trackball", e)

        self._setup_trackball_right_button()
        try:
            interactor = getattr(self._plotter, "interactor", None)
            if interactor is not None:
                interactor.installEventFilter(self)
        except Exception as e:
            log_exception(__name__, "_configure_mouse_controls.install_event_filter", e)

    def _setup_trackball_right_button(self) -> None:
        """Настроить trackball: ЛКМ drag выключен, ПКМ/СКМ разрешены."""
        if not self._plotter:
            return
        try:
            interactor = getattr(self._plotter, "interactor", None)
            style = (
                interactor.GetInteractorStyle()
                if interactor and hasattr(interactor, "GetInteractorStyle")
                else None
            )
            if style is not None:
                if hasattr(style, "SetLeftButtonMotion"):
                    style.SetLeftButtonMotion(False)
                if hasattr(style, "SetRightButtonMotion"):
                    style.SetRightButtonMotion(True)
                if hasattr(style, "SetMiddleButtonMotion"):
                    style.SetMiddleButtonMotion(True)
        except Exception as e:
            log_exception(__name__, "_setup_trackball_right_button.configure_style", e)

    def _pan_camera_by_pixels(self, 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

            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

            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.move_camera", e)
            return

    def eventFilter(self, watched, event):
        interactor = getattr(self._plotter, "interactor", None) if self._plotter else None
        if interactor is not None and watched is interactor:
            if event.type() == QEvent.Type.MouseButtonPress:
                if event.button() == Qt.MouseButton.RightButton:
                    self._cam_rotate_active = True
                    self._cam_last_pos = (event.position().x(), event.position().y())
                    return True
                if event.button() == Qt.MouseButton.MiddleButton:
                    self._cam_pan_active = True
                    self._cam_last_pos = (event.position().x(), event.position().y())
                    return True
                if event.button() == Qt.MouseButton.LeftButton:
                    return True

            if event.type() == QEvent.Type.MouseMove:
                if hasattr(event, "buttons") and (event.buttons() & Qt.MouseButton.LeftButton):
                    return True

                if (
                    self._cam_rotate_active
                    and hasattr(event, "buttons")
                    and (event.buttons() & Qt.MouseButton.RightButton)
                ):
                    cx, cy = self._cam_last_pos or (event.position().x(), event.position().y())
                    nx, ny = event.position().x(), event.position().y()
                    dx = float(nx - cx)
                    dy = float(ny - cy)
                    self._cam_last_pos = (nx, ny)
                    try:
                        cam = self._plotter.camera
                        cam.Azimuth(-dx * 0.35)
                        cam.Elevation(dy * 0.35)
                        cam.OrthogonalizeViewUp()
                        self._plotter.update()
                    except Exception as e:
                        log_exception(__name__, "eventFilter.rotate_camera", e)
                    return True

                if (
                    self._cam_pan_active
                    and hasattr(event, "buttons")
                    and (event.buttons() & Qt.MouseButton.MiddleButton)
                ):
                    cx, cy = self._cam_last_pos or (event.position().x(), event.position().y())
                    nx, ny = event.position().x(), event.position().y()
                    dx = float(nx - cx)
                    dy = float(ny - cy)
                    self._cam_last_pos = (nx, ny)
                    self._pan_camera_by_pixels(dx, dy)
                    try:
                        self._plotter.update()
                    except Exception as e:
                        log_exception(__name__, "eventFilter.pan_update", e)
                    return True

            if event.type() == QEvent.Type.MouseButtonRelease:
                if event.button() == Qt.MouseButton.RightButton:
                    self._cam_rotate_active = False
                    self._cam_last_pos = None
                    return True
                if event.button() == Qt.MouseButton.MiddleButton:
                    self._cam_pan_active = False
                    self._cam_last_pos = None
                    return True
                if event.button() == Qt.MouseButton.LeftButton:
                    return True
        return super().eventFilter(watched, event)


__all__ = ["ModelViewWidget"]


# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
#
# 1) Назначение модуля:
#    Автономный компактный 3D-просмотрщик STL-моделей, используемый
#    PartVisualizer. Управление мышью ограничено: ПКМ — вращение,
#    колёсико — зум, СКМ — панорамирование, ЛКМ отключён.
#
# 2) Зависимости модуля:
#    Импорты: math, Path (pathlib),
#             Qt, Signal, QEvent (PySide6.QtCore),
#             QWidget (PySide6.QtWidgets),
#             VContainer (gui.containers),
#             APP_STYLES (gui.styles)
#    Хост-класс / базовый класс: QWidget (не SContainer!)
#    Внешние библиотеки: pyvista (опционально), pyvistaqt (опционально).
#    При отсутствии — PYVISTA_AVAILABLE=False, 3D недоступен; вместо
#    ошибки выводится warning.
#
# 3) Экспорт:
#    Класс ModelViewWidget — компактный 3D-вьюер.
#    Сигнал: error_occurred(str).
#    Методы: load_model(path), clear_model(), set_isometric_view(),
#            set_top/front/left/right_view(), reset_view(),
#            zoom_in(), zoom_out().
#    __all__ = ["ModelViewWidget"].
#
# 4) Состояние (поля):
#    _plotter: QtInteractor|None — pyvista 3D-рендерер
#    _model_loaded: bool         — загружена ли модель
#    _cam_rotate_active: bool    — активно ли вращение ПКМ
#    _cam_pan_active: bool       — активно ли панорамирование СКМ
#    _cam_last_pos: tuple|None   — последняя позиция мыши
#    _model_layout: QVBoxLayout  — layout для plotter-виджета
#
# 5) Последовательность действий и вызовов:
#    __init__()
#      -> QWidget.__init__() -> _setup_ui()
#         -> VContainer() -> setLayout -> setStyleSheet
#    load_model(model_path)
#      -> проверка PYVISTA_AVAILABLE и Path.exists()
#      -> clear_model()
#      -> pv.read(path) -> QtInteractor -> add_mesh -> camera_position
#      -> _configure_mouse_controls()
#         -> enable_trackball_style()
#         -> _setup_trackball_right_button() — отключение ЛКМ drag
#         -> interactor.installEventFilter(self)
#    eventFilter(watched, event)
#      -> RightButton: _cam_rotate_active → Azimuth/Elevation
#      -> MiddleButton: _cam_pan_active → _pan_camera_by_pixels()
#      -> LeftButton: return True (блокировка)
#    _pan_camera_by_pixels(dx, dy)
#      -> вычисление right/up-вектора камеры
#      -> camera.SetPosition/SetFocalPoint — сдвиг в экранной плоскости
#
# 6) Побочные эффекты:
#    - Создаёт QtInteractor (тяжёлый OpenGL-контекст).
#    - Читает STL-файлы с диска.
#    - Перехватывает mouse events через eventFilter.
#    - Испускает error_occurred.
#
# 7) Границы ответственности:
#    Модуль — только предпросмотр. НЕ управляет зонами, сетками,
#    стеллажами. НЕ подключается к theme_bus. НЕ поддерживает
#    multiple mesh. НЕ наследует SContainer.
#
# 8) Обработка ошибок:
#    PYVISTA_AVAILABLE=False → _show_error при попытке загрузки.
#    load_model() try/except → _show_error → error_occurred.
#    clear_model() try/except при закрытии plotter.
#    eventFilter — все операции камеры обёрнуты в try/except.
#
# 9) Инварианты и контракты:
#    - _plotter может быть None до load_model().
#    - _model_loaded == True только если plotter успешно создан и mesh добавлен.
#    - ЛКМ всегда заблокирован (eventFilter возвращает True).
#    - Зум-коэффициенты: in=1.2, out=0.8.
#
# 10) Правило сопровождения:
#     При обновлении PyVista API — проверить enable_trackball_style(),
#     SetLeftButtonMotion, GetInteractorStyle. eventFilter — главная
#     точка кастомизации мышиного управления. Не наследовать от
#     SContainer — виджет намеренно легковесный (QWidget).
