# -*- coding: utf-8 -*-
# gui/components/model_view/_mv_model_loading.py
# Загрузка/очистка моделей

from __future__ import annotations

import threading
import time
from pathlib import Path
from typing import Optional, Callable, TYPE_CHECKING

from PySide6.QtWidgets import QApplication, QSizePolicy

from error_logger import log_exception

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

try:
    import pyvista as pv
    from pyvistaqt import QtInteractor
    PYVISTA_AVAILABLE = True
except ImportError:
    PYVISTA_AVAILABLE = False


class ModelLoadingMixin:
    """Загрузка моделей пола, стен, потолка и ферм."""

    # -- публичные ------------------------------------------------------------

    def load_facility_models(
        self: "ModelViewWidget",
        model_path: Path,
        facility_data: Optional[dict] = None,
        progress_callback: Optional[Callable[[int], None]] = None,
    ) -> bool:
        """Загрузка всех моделей помещения (пол, стены, стеллажи)."""
        if not PYVISTA_AVAILABLE:
            self.show_error("Библиотека PyVista не установлена")
            return False

        if not model_path.exists():
            self.show_error(f"Путь к моделям не найден: {model_path}")
            return False

        def _emit_progress(value: int) -> None:
            if progress_callback is None:
                return
            try:
                progress_callback(max(0, min(100, int(value))))
            except Exception as e:
                log_exception(__name__, "load_facility_models._emit_progress", e)

        self.clear_model()

        try:
            _emit_progress(1)
            self._plotter = QtInteractor(self)
            self._plotter.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
            self._plotter.setMinimumSize(0, 0)
            self._main_container.add_widget(self._plotter)
            QApplication.processEvents()
            if self._plotter.interactor is not None:
                self._plotter.interactor.installEventFilter(self)
            # Градиентный фон рендера (PyVista/VTK)
            self._plotter.set_background((0.84, 0.84, 0.84), top=(0.34, 0.34, 0.34))

            _emit_progress(5)
            missing: list[str] = []

            def _read_mesh_in_worker(path_str: str, progress_value: int):
                payload: dict[str, object] = {}

                def _worker() -> None:
                    try:
                        payload["mesh"] = pv.read(path_str)
                    except Exception as exc:  # pragma: no cover
                        payload["error"] = exc

                thread = threading.Thread(target=_worker, daemon=True)
                thread.start()
                # Поддерживаем отзывчивость UI во время тяжёлого парсинга STL.
                while thread.is_alive():
                    _emit_progress(progress_value)
                    try:
                        QApplication.processEvents()
                    except Exception as e:
                        log_exception(__name__, "load_facility_models._read_mesh_in_worker", e)
                    time.sleep(0.01)
                thread.join()
                if "error" in payload:
                    raise payload["error"]  # type: ignore[misc]
                return payload.get("mesh")

            def load_model(
                key: str,
                rel_path: Optional[str],
                color: str,
                opacity: float,
                progress_start: int,
                progress_end: int,
            ):
                _emit_progress(progress_start)
                if not rel_path:
                    _emit_progress(progress_end)
                    return
                rel = Path(rel_path)
                if rel.is_absolute():
                    file_path = rel
                else:
                    if rel_path.startswith("facility/"):
                        base_root = model_path.parent.parent
                        file_path = base_root / rel
                    else:
                        file_path = model_path / rel
                if not file_path.exists():
                    missing.append(rel_path)
                    _emit_progress(progress_end)
                    return
                try:
                    mesh = _read_mesh_in_worker(str(file_path), progress_start)
                    if mesh is None:
                        _emit_progress(progress_end)
                        return
                    actor = self._plotter.add_mesh(
                        mesh, color=color, opacity=opacity, show_edges=False,
                    )
                    self._model_actors[key] = actor
                    if key == "floor":
                        self._floor_mesh = mesh
                    elif key == "walls":
                        self._walls_mesh = mesh
                    elif key == "ceiling":
                        self._ceiling_mesh = mesh
                    elif key == "truss":
                        self._truss_mesh = mesh
                    print(f"Загружена модель {key}: {file_path}")
                except Exception as e:
                    log_exception(__name__, "load_model", e)
            floor_path = facility_data.get("facility_floor") if facility_data else "floor.stl"
            walls_path = facility_data.get("facility_walls") if facility_data else "walls.stl"
            ceiling_path = facility_data.get("facility_ceiling") if facility_data else None
            truss_path = facility_data.get("facility_truss") if facility_data else None

            model_jobs = [
                ("floor", floor_path, "#CCCCCC", 0.8),
                ("walls", walls_path, "#AAAAAA", 0.6),
                ("ceiling", ceiling_path, "#B0B0B0", 0.5),
                ("truss", truss_path, "#888888", 0.5),
            ]
            total_jobs = max(1, len(model_jobs))
            for idx, (key, rel_path, color, opacity) in enumerate(model_jobs, start=1):
                start_p = 10 + int(((idx - 1) / total_jobs) * 60)
                end_p = 10 + int((idx / total_jobs) * 60)
                load_model(key, rel_path, color, opacity, start_p, end_p)
                _emit_progress(end_p)

            # Собираем уникальные вершины для выбора углов
            try:
                points: list[tuple[float, ...]] = []
                for mesh in (self._floor_mesh, self._walls_mesh, self._ceiling_mesh, self._truss_mesh):
                    if mesh is None:
                        continue
                    if hasattr(mesh, "points"):
                        points.extend(mesh.points.tolist())
                unique: list[tuple[float, float, float]] = []
                seen: set[tuple[float, float, float]] = set()
                for px, py, pz in points:
                    key = (round(px, 3), round(py, 3), round(pz, 3))
                    if key in seen:
                        continue
                    seen.add(key)
                    unique.append((px, py, pz))
                self.set_corner_points(unique)
            except Exception as e:
                log_exception(__name__, "load_facility_models", e)
            _emit_progress(75)

            # Рассчитываем bounding box помещения
            self._calculate_room_bounds()
            _emit_progress(82)

            # Настраиваем стартовый изометрический вид facility.
            self._apply_facility_isometric_view(reset_camera=True, update=False)
            self._plotter.enable_trackball_style()
            self._setup_trackball_right_button()
            _emit_progress(90)

            # Подключаем обработчик кликов
            self._plotter.track_click_position(
                callback=self._on_plotter_click,
                side="left",
                double=False,
            )
            self._plotter.track_click_position(
                callback=self._on_plotter_double_click,
                side="left",
                double=True,
            )
            self._plotter.track_click_position(
                callback=self._on_plotter_right_click,
                side="right",
                double=False,
            )
            _emit_progress(95)
            self._models_loaded = True
            self._reset_camera_clipping_range()

            print(
                f"Загружено моделей: пол={self._floor_mesh is not None}, "
                f"стены={self._walls_mesh is not None}, "
                f"потолок={'ceiling' in self._model_actors}, "
                f"фермы={'truss' in self._model_actors}"
            )

            self._missing_models = set(missing)
            _emit_progress(100)
            return True

        except Exception as e:
            error_msg = f"Ошибка загрузки моделей: {e}"
            print(error_msg)
            self.show_error(error_msg)
            return False

    # -- границы --------------------------------------------------------------

    def _calculate_room_bounds(self: "ModelViewWidget") -> None:
        """Рассчёт bounding box помещения на основе моделей."""
        if not self._floor_mesh:
            return
        try:
            bounds = self._floor_mesh.bounds
            if self._walls_mesh:
                wb = self._walls_mesh.bounds
                bounds = (
                    min(bounds[0], wb[0]),
                    max(bounds[1], wb[1]),
                    min(bounds[2], wb[2]),
                    max(bounds[3], wb[3]),
                    min(bounds[4], wb[4]),
                    max(bounds[5], wb[5]),
                )
            for mesh in (self._ceiling_mesh, self._truss_mesh):
                if mesh is None:
                    continue
                mb = mesh.bounds
                bounds = (
                    min(bounds[0], mb[0]),
                    max(bounds[1], mb[1]),
                    min(bounds[2], mb[2]),
                    max(bounds[3], mb[3]),
                    min(bounds[4], mb[4]),
                    max(bounds[5], mb[5]),
                )
            self._room_bounds = bounds
            print(f"Room bounds: {bounds}")
        except Exception as e:
            print(f"Ошибка расчета bounding box: {e}")
            self._room_bounds = None

    # -- очистка / видимость --------------------------------------------------

    def clear_model(self: "ModelViewWidget") -> None:
        """Очистка области модели."""
        self._models_loaded = False
        try:
            self.clear_all_racks()
        except Exception as e:
            log_exception(__name__, "clear_model", e)

        if self._plotter is not None:
            try:
                interactor = getattr(self._plotter, "interactor", None)
                if interactor is not None:
                    try:
                        interactor.removeEventFilter(self)
                    except Exception as e:
                        log_exception(__name__, "clear_model", e)
                self._plotter.close()
            except Exception as e:
                log_exception(__name__, "clear_model", e)
            self._plotter = None

        if hasattr(self, "_main_container") and self._main_container is not None:
            content_host = getattr(self._main_container, "_content_host", None)
            if content_host is not None:
                layout = content_host.get_layout()
                while layout.count():
                    item = layout.takeAt(0)
                    w = item.widget()
                    if w is not None:
                        w.setParent(None)
                        w.deleteLater()

        self._zones.clear()
        self._zone_data.clear()
        self._floor_mesh = None
        self._walls_mesh = None
        self._ceiling_mesh = None
        self._truss_mesh = None
        self._rack_meshes.clear()
        self._room_bounds = None
        self._model_actors.clear()

    def _reload_scene(self: "ModelViewWidget") -> None:
        """Перезагрузка сцены (используется при удалении зон)."""
        if not self._models_loaded:
            return
        current_camera = self._plotter.camera_position  # noqa: F841
        zones_backup = list(self._zones.items())  # noqa: F841
        self.clear_model()
        print("Требуется перезагрузка моделей для обновления зон")

    def _reset_camera_clipping_range(self: "ModelViewWidget") -> None:
        """Обновить near/far clipping range камеры по текущей сцене."""
        if not self._plotter:
            return
        try:
            renderer = getattr(self._plotter, "renderer", None)
            if renderer is not None:
                renderer.ResetCameraClippingRange()
            else:
                reset_fn = getattr(self._plotter, "reset_camera_clipping_range", None)
                if callable(reset_fn):
                    reset_fn()
        except Exception as e:
            log_exception(__name__, "_reset_camera_clipping_range", e)

    def set_model_visibility(self: "ModelViewWidget", key: str, visible: bool) -> None:
        actor = self._model_actors.get(key)
        if actor is None:
            return
        try:
            actor.SetVisibility(1 if visible else 0)
            self._plotter.update()
        except Exception as e:
            log_exception(__name__, "set_model_visibility", e)

    def has_model(self: "ModelViewWidget", key: str) -> bool:
        return key in self._model_actors

    def is_model_visible(self: "ModelViewWidget", key: str) -> bool:
        actor = self._model_actors.get(key)
        if actor is None:
            return False
        try:
            return bool(actor.GetVisibility())
        except Exception as e:
            log_exception(__name__, "is_model_visible", e)
            return False


# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
#    Загрузка и очистка моделей помещения, пересборка сцены и управление видимостью.
#
# 2) Последовательность действий и вызовов:
#    A. Класс ModelLoadingMixin: точки входа
#       Публичные методы сценария:
#       - ModelLoadingMixin.load_facility_models(...)
#       - ModelLoadingMixin.clear_model(...)
#       - ModelLoadingMixin.set_model_visibility(...)
#       - ModelLoadingMixin.has_model(...)
#       - ModelLoadingMixin.is_model_visible(...)
#
#    B. ModelLoadingMixin: запуск и настройка:
#       ModelLoadingMixin.load_facility_models(...)
#         Назначение: Загрузка всех моделей помещения (пол, стены, стеллажи).
#         Последовательность внутренних вызовов:
#         -> ModelLoadingMixin.clear_model(...)
#         -> ModelLoadingMixin._calculate_room_bounds(...)
#         -> ModelLoadingMixin._reset_camera_clipping_range(...)
#       ModelLoadingMixin.set_model_visibility(...)
#         Назначение: устанавливает model visibility в рамках текущего сценария модуля.
#
#    C. ModelLoadingMixin: основной сценарий:
#       ModelLoadingMixin.has_model(...)
#         Назначение: проверяет наличие model в рамках текущего сценария модуля.
#       ModelLoadingMixin.is_model_visible(...)
#         Назначение: проверяет, что model visible в рамках текущего сценария модуля.
#
#    D. ModelLoadingMixin: завершение и очистка:
#       ModelLoadingMixin.clear_model(...)
#         Назначение: Очистка области модели.
#
#    E. ModelLoadingMixin: вспомогательные расчёты:
#       ModelLoadingMixin._calculate_room_bounds(...)
#         Назначение: Рассчёт bounding box помещения на основе моделей.
#       ModelLoadingMixin._reload_scene(...)
#         Назначение: Перезагрузка сцены (используется при удалении зон).
#         Последовательность внутренних вызовов:
#         -> ModelLoadingMixin.clear_model(...)
#       ModelLoadingMixin._reset_camera_clipping_range(...)
#         Назначение: Обновить near/far clipping range камеры по текущей сцене.
#
# 3) Важные ограничения и инварианты:
#    - Модуль выполняется в составе ModelViewWidget и использует согласованные поля состояния self._... .
#    - Все операции с актёрами и камерой выполняются только при валидном self._plotter с безопасной обработкой исключений.
#    - Геометрическая визуализация зависит от pyvista/vtk; при недоступности модуль обязан завершать шаг без падения сценария.
#    - Межмодульная связность: только через фасад model_view; прямые обращения между zone, rack, shelf, cell запрещены.
#    - Очистка состояния должна быть идемпотентной: повторный вызов не меняет корректное состояние в ошибочное.
#
# 4) Правило сопровождения:
#    - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
#    - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).
