# -*- coding: utf-8 -*-
# gui/components/_mv/_mv_visual.py
# Маркеры, квадранты, оси, координатные преобразования

from __future__ import annotations

import math
from typing import Optional, Tuple, TYPE_CHECKING
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
    _PV = True
except ImportError:
    _PV = False


class VisualHelpersMixin:
    """Маркер начала координат, квадранты, оси, world↔display преобразования."""

    # -- маркер начала координат ----------------------------------------------

    _ORIGIN_MARKER_NAME = "__origin_marker"
    _ORIGIN_PREVIEW_MARKER_NAME = "__origin_preview_marker"

    def show_origin_marker(self: "ModelViewWidget", x: float, y: float, z: float) -> None:
        if not self._plotter:
            return
        try:
            self._plotter.remove_actor(self._ORIGIN_MARKER_NAME)
        except Exception as _exc:
            log_exception(__name__, "show_origin_marker", _exc)
        try:
            sphere = pv.Sphere(radius=80, center=(x, y, z))
            self._origin_marker = self._plotter.add_mesh(
                sphere, color=(1, 1, 0), opacity=0.8, name=self._ORIGIN_MARKER_NAME,
            )
            self._plotter.update()
        except Exception as _exc:
            log_exception(__name__, "show_origin_marker", _exc)
    def show_origin_preview_marker(self: "ModelViewWidget", x: float, y: float, z: float) -> None:
        """Показать динамический маркер origin, следующий за курсором."""
        if not self._plotter:
            return
        try:
            self._plotter.remove_actor(self._ORIGIN_PREVIEW_MARKER_NAME)
        except Exception as _exc:
            log_exception(__name__, "show_origin_preview_marker", _exc)
        try:
            sphere = pv.Sphere(radius=55, center=(x, y, z))
            self._origin_preview_marker = self._plotter.add_mesh(
                sphere,
                color=(0.2, 0.9, 1.0),
                opacity=0.7,
                name=self._ORIGIN_PREVIEW_MARKER_NAME,
            )
            self._plotter.update()
        except Exception as _exc:
            log_exception(__name__, "show_origin_preview_marker", _exc)
    def clear_origin_marker(self: "ModelViewWidget") -> None:
        if not self._plotter:
            return
        try:
            self._plotter.remove_actor(self._ORIGIN_MARKER_NAME)
        except Exception as _exc:
            log_exception(__name__, "clear_origin_marker", _exc)
        self._origin_marker = None
        try:
            if hasattr(self, "_safe_render"):
                self._safe_render(min_interval_s=0.05)
            else:
                self._plotter.render()
        except Exception as _exc:
            log_exception(__name__, "clear_origin_marker", _exc)
    def clear_origin_preview_marker(self: "ModelViewWidget") -> None:
        if not self._plotter:
            return
        try:
            self._plotter.remove_actor(self._ORIGIN_PREVIEW_MARKER_NAME)
        except Exception as _exc:
            log_exception(__name__, "clear_origin_preview_marker", _exc)
        self._origin_preview_marker = None
        try:
            if hasattr(self, "_safe_render"):
                self._safe_render(min_interval_s=0.05)
            else:
                self._plotter.render()
        except Exception as _exc:
            log_exception(__name__, "clear_origin_preview_marker", _exc)
    def clear_origin_markers(self: "ModelViewWidget") -> None:
        """Очистить фиксированный и динамический маркеры origin."""
        self.clear_origin_preview_marker()
        self.clear_origin_marker()

    # -- угловые точки --------------------------------------------------------

    def set_corner_points(self: "ModelViewWidget", points: list[tuple[float, float, float]]) -> None:
        self._corner_points = list(points)

    def get_corner_points(self: "ModelViewWidget") -> list[tuple[float, float, float]]:
        return list(self._corner_points)

    # -- квадранты -------------------------------------------------------------

    def show_quadrants(self: "ModelViewWidget", origin: Tuple[float, float, float], size: float = 6000.0) -> None:
        if not self._plotter:
            return
        self.clear_quadrants()
        x0, y0, z0 = origin
        half = size / 2.0
        # Фиксированная высота квадранта: 100 мм.
        z_min = z0
        z_max = z0 + 100.0

        quadrant_bounds = [
            ((x0, x0 + half), (y0, y0 + half)),      # 0: ++
            ((x0 - half, x0), (y0, y0 + half)),      # 1: -+
            ((x0 - half, x0), (y0 - half, y0)),      # 2: --
            ((x0, x0 + half), (y0 - half, y0)),       # 3: +-
        ]

        colors = [(0.6, 0.6, 0.6)] * 4

        for idx, (x_range, y_range) in enumerate(quadrant_bounds):
            x_min, x_max = x_range
            y_min, y_max = y_range
            box = pv.Box(bounds=(x_min, x_max, y_min, y_max, z_min, z_max))
            name = f"__quadrant_plane_{idx}"
            actor = self._plotter.add_mesh(
                box,
                color=colors[idx],
                opacity=0.12,
                show_edges=True,
                edge_color=(0.7, 0.7, 0.7),
                line_width=1.0,
                name=name,
            )
            try:
                actor.PickableOn()
            except Exception as _exc:
                log_exception(__name__, "show_quadrants", _exc)
            self._quadrant_actors.append(actor)

        for idx, (x_range, y_range) in enumerate(quadrant_bounds):
            x_min, x_max = x_range
            y_min, y_max = y_range
            center_x = (x_min + x_max) / 2.0
            center_y = (y_min + y_max) / 2.0
            label_pos = [center_x, center_y, z0 + 100]

            name = f"__quadrant_label_{idx}"
            label_actor = self._plotter.add_point_labels(
                [label_pos],
                [f"Q{idx}"],
                font_size=72,
                text_color="yellow",
                point_color="yellow",
                point_size=30,
                render_points_as_spheres=True,
                always_visible=True,
                shape_opacity=0.9,
                name=name,
            )
            try:
                if label_actor is not None:
                    if hasattr(label_actor, "PickableOff"):
                        label_actor.PickableOff()
                    elif hasattr(label_actor, "SetPickable"):
                        label_actor.SetPickable(False)
            except Exception as _exc:
                log_exception(__name__, "show_quadrants", _exc)
            self._quadrant_label_actors.append(name)

        self._plotter.update()

    def animate_focus_on_quadrants(
        self: "ModelViewWidget",
        *,
        duration_ms: int = 420,
        margin_factor: float = 1.2,
    ) -> bool:
        """Плавно сфокусировать камеру так, чтобы текущие квадранты целиком были в кадре."""
        if not self._plotter:
            return False
        cam = getattr(self._plotter, "camera", None)
        if cam is None:
            return False
        actors = list(getattr(self, "_quadrant_actors", []) or [])
        if not actors:
            return False
        try:
            min_x = float("inf")
            max_x = float("-inf")
            min_y = float("inf")
            max_y = float("-inf")
            min_z = float("inf")
            max_z = float("-inf")
            for actor in actors:
                if actor is None or not hasattr(actor, "GetBounds"):
                    continue
                b = actor.GetBounds()
                if not b or len(b) < 6:
                    continue
                min_x = min(min_x, float(b[0]))
                max_x = max(max_x, float(b[1]))
                min_y = min(min_y, float(b[2]))
                max_y = max(max_y, float(b[3]))
                min_z = min(min_z, float(b[4]))
                max_z = max(max_z, float(b[5]))
            if not all(math.isfinite(v) for v in (min_x, max_x, min_y, max_y, min_z, max_z)):
                return False
            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) * max(1.0, float(margin_factor))
            cz = min_z + (sz * 0.55)

            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())

            vx, vy, vz = 1.0, -1.0, 0.72
            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
            distance = max(3000.0, span * 2.35)
            target_focal = (cx, cy, cz)
            target_pos = (
                cx + vx * distance,
                cy + vy * distance,
                cz + vz * distance + (sz * 0.08),
            )

            if hasattr(self, "_animate_camera_transition"):
                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=(ux, uy, uz),
                    duration_ms=int(duration_ms),
                    steps=18,
                )
            else:
                cam.SetPosition(*target_pos)
                cam.SetFocalPoint(*target_focal)
                if hasattr(cam, "SetViewUp"):
                    cam.SetViewUp(ux, uy, uz)
                if hasattr(self, "_reset_camera_clipping_range"):
                    self._reset_camera_clipping_range()
                self._plotter.update()
            return True
        except Exception as exc:
            log_exception(__name__, "animate_focus_on_quadrants", exc)
            return False

    def show_grid_step_quadrants(
        self: "ModelViewWidget",
        origin: Tuple[float, float, float],
        direction: Tuple[float, float],
        step_values: list[int],
        cell_size: float = 3000.0,
        height: float = 100.0,
    ) -> None:
        """Показать 6 площадок выбора шага сетки (3x2) в положительных локальных координатах."""
        if not self._plotter:
            return
        self.clear_quadrants()
        x0, y0, z0 = origin
        dir_x = 1.0 if float(direction[0]) >= 0.0 else -1.0
        dir_y = 1.0 if float(direction[1]) >= 0.0 else -1.0
        z_min = float(z0)
        z_max = float(z0) + float(height)
        grid_actors = list(getattr(self, "_quadrant_grid_actors", []) or [])
        for actor in grid_actors:
            try:
                self._plotter.remove_actor(actor)
            except Exception as _exc:
                log_exception(__name__, "show_grid_step_quadrants", _exc)
        self._quadrant_grid_actors = []

        def _add_segment(
            pts: list[list[float]],
            cells: list[int],
            p1: tuple[float, float, float],
            p2: tuple[float, float, float],
        ) -> None:
            i1 = len(pts)
            pts.append([float(p1[0]), float(p1[1]), float(p1[2])])
            i2 = len(pts)
            pts.append([float(p2[0]), float(p2[1]), float(p2[2])])
            cells.extend([2, i1, i2])

        for idx, step in enumerate(step_values):
            row = idx // 3
            col = idx % 3
            lx0 = float(col) * float(cell_size)
            lx1 = float(col + 1) * float(cell_size)
            ly0 = float(row) * float(cell_size)
            ly1 = float(row + 1) * float(cell_size)
            wx0 = float(x0) + lx0 * dir_x
            wx1 = float(x0) + lx1 * dir_x
            wy0 = float(y0) + ly0 * dir_y
            wy1 = float(y0) + ly1 * dir_y
            x_min, x_max = (min(wx0, wx1), max(wx0, wx1))
            y_min, y_max = (min(wy0, wy1), max(wy0, wy1))

            box = pv.Box(bounds=(x_min, x_max, y_min, y_max, z_min, z_max))
            name = f"__quadrant_plane_{idx}"
            actor = self._plotter.add_mesh(
                box,
                color=(0.6, 0.6, 0.6),
                opacity=0.12,
                show_edges=True,
                edge_color=(0.7, 0.7, 0.7),
                line_width=1.0,
                name=name,
            )
            try:
                actor.PickableOn()
            except Exception as _exc:
                log_exception(__name__, "_add_segment", _exc)
            self._quadrant_actors.append(actor)

            # Явная разметка сеткой с шагом площадки (поверх плоскости).
            line_points: list[list[float]] = []
            line_cells: list[int] = []
            z_grid = float(z_max) + 0.8
            step_size = max(10.0, float(step))
            x_pos = float(x_min)
            while x_pos <= float(x_max) + 1e-6:
                _add_segment(line_points, line_cells, (x_pos, y_min, z_grid), (x_pos, y_max, z_grid))
                x_pos += step_size
            y_pos = float(y_min)
            while y_pos <= float(y_max) + 1e-6:
                _add_segment(line_points, line_cells, (x_min, y_pos, z_grid), (x_max, y_pos, z_grid))
                y_pos += step_size
            if line_points and line_cells:
                try:
                    grid_lines = pv.PolyData(line_points)
                    grid_lines.lines = line_cells
                    grid_actor = self._plotter.add_mesh(
                        grid_lines,
                        color=(0.0, 0.95, 1.0),
                        line_width=2.0,
                        opacity=0.95,
                        lighting=False,
                    )
                    self._quadrant_grid_actors.append(grid_actor)
                except Exception as _exc:
                    log_exception(__name__, "_add_segment", _exc)
            label_pos = [(x_min + x_max) / 2.0, (y_min + y_max) / 2.0, float(z0) + float(height)]
            label_name = f"__quadrant_label_{idx}"
            label_actor = self._plotter.add_point_labels(
                [label_pos],
                [f"{int(step)} мм"],
                font_size=56,
                text_color="yellow",
                point_color="yellow",
                point_size=24,
                render_points_as_spheres=True,
                always_visible=True,
                shape_opacity=0.9,
                name=label_name,
            )
            try:
                if label_actor is not None:
                    if hasattr(label_actor, "PickableOff"):
                        label_actor.PickableOff()
                    elif hasattr(label_actor, "SetPickable"):
                        label_actor.SetPickable(False)
            except Exception as _exc:
                log_exception(__name__, "_add_segment", _exc)
            self._quadrant_label_actors.append(label_name)

        self._plotter.update()

    def highlight_quadrant(self: "ModelViewWidget", index: int) -> None:
        if not self._plotter:
            return
        for i, actor in enumerate(self._quadrant_actors):
            try:
                if i == index:
                    actor.GetProperty().SetOpacity(1.0)
                    actor.GetProperty().SetColor(1.0, 1.0, 0.0)
                else:
                    actor.GetProperty().SetOpacity(0.12)
                    actor.GetProperty().SetColor(0.6, 0.6, 0.6)
            except Exception as _exc:
                log_exception(__name__, "highlight_quadrant", _exc)
        try:
            if hasattr(self, "_safe_render"):
                self._safe_render(min_interval_s=0.01)
            else:
                self._plotter.render()
        except Exception as _exc:
            log_exception(__name__, "highlight_quadrant", _exc)
    def clear_quadrants(self: "ModelViewWidget") -> None:
        if not self._plotter:
            return
        for actor in self._quadrant_actors:
            try:
                self._plotter.remove_actor(actor)
            except Exception as _exc:
                log_exception(__name__, "clear_quadrants", _exc)
        self._quadrant_actors = []

        for name in self._quadrant_label_actors:
            try:
                self._plotter.remove_actor(name)
            except Exception as _exc:
                log_exception(__name__, "clear_quadrants", _exc)
        self._quadrant_label_actors = []
        for actor in list(getattr(self, "_quadrant_grid_actors", []) or []):
            try:
                self._plotter.remove_actor(actor)
            except Exception as _exc:
                log_exception(__name__, "clear_quadrants", _exc)
        self._quadrant_grid_actors = []

        try:
            if hasattr(self, "_safe_render"):
                self._safe_render(min_interval_s=0.05)
            else:
                self._plotter.render()
        except Exception as _exc:
            log_exception(__name__, "clear_quadrants", _exc)
    # -- оси -------------------------------------------------------------------

    def show_axes(self: "ModelViewWidget", origin: Tuple[float, float, float], dir_x: float, dir_y: float) -> None:
        if not self._plotter:
            return
        for actor in self._axes_actors:
            try:
                self._plotter.remove_actor(actor)
            except Exception as _exc:
                log_exception(__name__, "show_axes", _exc)
        self._axes_actors = []
        x0, y0, z0 = origin
        axes = [
            ((dir_x, 0, 0), (1, 0, 0), "X"),     # X: red
            ((0, dir_y, 0), (0, 1, 0), "Y"),     # Y: green
            ((0, 0, 1), (0, 0, 1), "Z"),         # Z-up: blue
        ]
        axis_len = 1500.0
        label_offset = 1700.0
        for direction, color, label in axes:
            arrow = pv.Arrow(start=(x0, y0, z0), direction=direction, scale=axis_len)
            actor = self._plotter.add_mesh(
                arrow,
                color=color,
                opacity=1.0,
                lighting=False,
                ambient=1.0,
                diffuse=0.0,
                specular=0.0,
            )
            self._axes_actors.append(actor)
            try:
                dx, dy, dz = float(direction[0]), float(direction[1]), float(direction[2])
                label_pos = [[x0 + dx * label_offset, y0 + dy * label_offset, z0 + dz * label_offset]]
                label_actor = self._plotter.add_point_labels(
                    label_pos,
                    [label],
                    font_size=36,
                    text_color=color,
                    point_size=1,
                    shape_opacity=0.0,
                    always_visible=True,
                )
                if label_actor is not None:
                    self._axes_actors.append(label_actor)
            except Exception as _exc:
                log_exception(__name__, "show_axes", _exc)
        try:
            if hasattr(self, "_reset_camera_clipping_range"):
                self._reset_camera_clipping_range()
        except Exception as _exc:
            log_exception(__name__, "show_axes", _exc)
        try:
            if hasattr(self, "_safe_render"):
                self._safe_render(min_interval_s=0.0)
            else:
                self._plotter.render()
        except Exception as _exc:
            log_exception(__name__, "show_axes", _exc)
    # -- преобразования координат ----------------------------------------------

    def world_to_display(self: "ModelViewWidget", x: float, y: float, z: float) -> Optional[Tuple[float, float]]:
        if not self._plotter or not self._plotter.renderer:
            return None
        try:
            self._plotter.renderer.SetWorldPoint(x, y, z, 1.0)
            self._plotter.renderer.WorldToDisplay()
            dx, dy, _ = self._plotter.renderer.GetDisplayPoint()
            return (dx, dy)
        except Exception as exc:
            log_exception(__name__, "world_to_display", exc)
            return None

    def pick_world(self: "ModelViewWidget", sx: float, sy: float) -> Optional[Tuple[float, float, float]]:
        if not self._plotter or not self._plotter.renderer or not self._plotter.picker:
            return None
        try:
            self._plotter.picker.Pick(sx, sy, 0, self._plotter.renderer)
            pos = self._plotter.picker.GetPickPosition()
            if pos is None:
                return None
            x, y, z = pos[0], pos[1], pos[2]
            if all(math.isfinite(v) for v in (x, y, z)):
                return (x, y, z)
        except Exception as exc:
            log_exception(__name__, "pick_world", exc)
            return None
        return None

    def screen_to_world_on_plane(
        self: "ModelViewWidget", sx: float, sy: float, z_plane: float,
    ) -> Optional[Tuple[float, float, float]]:
        """Преобразовать экранные координаты в мировые на плоскости Z=z_plane."""
        if not self._plotter or not self._plotter.renderer:
            return None
        try:
            renderer = self._plotter.renderer
            renderer.SetDisplayPoint(sx, sy, 0)
            renderer.DisplayToWorld()
            p0 = renderer.GetWorldPoint()
            renderer.SetDisplayPoint(sx, sy, 1)
            renderer.DisplayToWorld()
            p1 = renderer.GetWorldPoint()
            if p0[3] == 0 or p1[3] == 0:
                return None
            p0 = (p0[0] / p0[3], p0[1] / p0[3], p0[2] / p0[3])
            p1 = (p1[0] / p1[3], p1[1] / p1[3], p1[2] / p1[3])

            dz = p1[2] - p0[2]
            if dz == 0:
                return None
            t = (z_plane - p0[2]) / dz
            wx = p0[0] + t * (p1[0] - p0[0])
            wy = p0[1] + t * (p1[1] - p0[1])
            return (wx, wy, z_plane)
        except Exception as exc:
            log_exception(__name__, "screen_to_world_on_plane", exc)
            return None


# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
#    Визуальные вспомогательные функции: маркеры, оси, перевод координат, подбор точек.
#
# 2) Последовательность действий и вызовов:
#    A. Класс VisualHelpersMixin: точки входа
#       Публичные методы сценария:
#       - VisualHelpersMixin.show_origin_marker(...)
#       - VisualHelpersMixin.show_origin_preview_marker(...)
#       - VisualHelpersMixin.clear_origin_marker(...)
#       - VisualHelpersMixin.clear_origin_preview_marker(...)
#       - VisualHelpersMixin.clear_origin_markers(...)
#       - VisualHelpersMixin.set_corner_points(...)
#       - VisualHelpersMixin.get_corner_points(...)
#       - VisualHelpersMixin.show_quadrants(...)
#       - VisualHelpersMixin.highlight_quadrant(...)
#       - VisualHelpersMixin.clear_quadrants(...)
#       - VisualHelpersMixin.show_axes(...)
#       - VisualHelpersMixin.world_to_display(...)
#       - VisualHelpersMixin.pick_world(...)
#       - VisualHelpersMixin.screen_to_world_on_plane(...)
#
#    B. VisualHelpersMixin: запуск и настройка:
#       VisualHelpersMixin.set_corner_points(...)
#         Назначение: устанавливает corner points в рамках текущего сценария модуля.
#
#    C. VisualHelpersMixin: основной сценарий:
#       VisualHelpersMixin.show_origin_marker(...)
#         Назначение: показывает origin marker в рамках текущего сценария модуля.
#       VisualHelpersMixin.show_origin_preview_marker(...)
#         Назначение: показывает динамический маркер origin в рамках текущего сценария модуля.
#       VisualHelpersMixin.show_quadrants(...)
#         Назначение: показывает quadrants в рамках текущего сценария модуля.
#         Последовательность внутренних вызовов:
#         -> VisualHelpersMixin.clear_quadrants(...)
#       VisualHelpersMixin.show_axes(...)
#         Назначение: показывает axes в рамках текущего сценария модуля.
#       VisualHelpersMixin.pick_world(...)
#         Назначение: выполняет шаг "pick world" в рамках текущего сценария модуля.
#
#    D. VisualHelpersMixin: завершение и очистка:
#       VisualHelpersMixin.clear_origin_marker(...)
#         Назначение: очищает origin marker в рамках текущего сценария модуля.
#       VisualHelpersMixin.clear_origin_preview_marker(...)
#         Назначение: очищает origin preview marker в рамках текущего сценария модуля.
#       VisualHelpersMixin.clear_origin_markers(...)
#         Назначение: очищает все маркеры origin в рамках текущего сценария модуля.
#       VisualHelpersMixin.clear_quadrants(...)
#         Назначение: очищает quadrants в рамках текущего сценария модуля.
#
#    E. VisualHelpersMixin: вспомогательные расчёты:
#       VisualHelpersMixin.get_corner_points(...)
#         Назначение: возвращает corner points в рамках текущего сценария модуля.
#       VisualHelpersMixin.highlight_quadrant(...)
#         Назначение: подсвечивает quadrant в рамках текущего сценария модуля.
#       VisualHelpersMixin.world_to_display(...)
#         Назначение: выполняет шаг "world to display" в рамках текущего сценария модуля.
#       VisualHelpersMixin.screen_to_world_on_plane(...)
#         Назначение: Преобразовать экранные координаты в мировые на плоскости Z=z_plane.
#
# 3) Важные ограничения и инварианты:
#    - Модуль выполняется в составе ModelViewWidget и использует согласованные поля состояния self._... .
#    - Все операции с актёрами и камерой выполняются только при валидном self._plotter с безопасной обработкой исключений.
#    - Геометрическая визуализация зависит от pyvista/vtk; при недоступности модуль обязан завершать шаг без падения сценария.
#    - Межмодульная связность: только через фасад model_view; прямые обращения между zone, rack, shelf, cell запрещены.
#    - Очистка состояния должна быть идемпотентной: повторный вызов не меняет корректное состояние в ошибочное.
#
# 4) Правило сопровождения:
#    - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
#    - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).
