# -*- coding: utf-8 -*-
"""Вспомогательные функции камеры для изометрической фокусировки на стеллаже / слоте полки и анимированных переходов."""

from __future__ import annotations

import math
from typing import TYPE_CHECKING

from PySide6.QtCore import QTimer

from error_logger import log_exception

if TYPE_CHECKING:
    from gui.components.model_view_widget import ModelViewWidget


class RackCameraMixin:
    """Миксин с утилитами фокусировки камеры и анимации для видов стеллажей."""

    def _compute_isometric_fit_distance(
        self: "ModelViewWidget",
        bounds: tuple[float, float, float, float, float, float],
        *,
        view_dir: tuple[float, float, float],
        up_dir: tuple[float, float, float] = (0.0, 0.0, 1.0),
        margin_factor: float = 1.10,
        min_distance: float = 700.0,
    ) -> float:
        """Вернуть дистанцию камеры, чтобы bbox гарантированно помещался в кадр."""
        if not self._plotter:
            return float(min_distance)
        cam = getattr(self._plotter, "camera", None)
        if cam is None:
            return float(min_distance)

        min_x, max_x, min_y, max_y, min_z, max_z = [float(v) for v in bounds]
        ex = max(0.5, (max_x - min_x) * 0.5)
        ey = max(0.5, (max_y - min_y) * 0.5)
        ez = max(0.5, (max_z - min_z) * 0.5)

        vx, vy, vz = [float(v) for v in view_dir]
        ux, uy, uz = [float(v) for v in up_dir]
        v_norm = max(1e-6, (vx * vx + vy * vy + vz * vz) ** 0.5)
        u_norm = max(1e-6, (ux * ux + uy * uy + uz * uz) ** 0.5)
        vx, vy, vz = vx / v_norm, vy / v_norm, vz / v_norm
        ux, uy, uz = ux / u_norm, uy / u_norm, uz / u_norm

        fx, fy, fz = -vx, -vy, -vz
        rx = fy * uz - fz * uy
        ry = fz * ux - fx * uz
        rz = fx * uy - fy * ux
        r_norm = max(1e-6, (rx * rx + ry * ry + rz * rz) ** 0.5)
        rx, ry, rz = rx / r_norm, ry / r_norm, rz / r_norm

        half_w = abs(rx) * ex + abs(ry) * ey + abs(rz) * ez
        half_h = abs(ux) * ex + abs(uy) * ey + abs(uz) * ez

        view_angle_deg = 30.0
        try:
            view_angle_deg = float(cam.GetViewAngle())
        except Exception as e:
            log_exception(__name__, "_compute_isometric_fit_distance", e)
        if view_angle_deg <= 1e-3:
            view_angle_deg = 30.0
        half_vfov = (view_angle_deg * 0.5) * (3.141592653589793 / 180.0)
        tan_half_v = max(1e-4, math.tan(half_vfov))

        aspect = 1.0
        try:
            ren_win = self._plotter.ren_win
            if ren_win:
                w, h = ren_win.GetSize()
                if h:
                    aspect = max(1e-3, float(w) / float(h))
        except Exception as e:
            log_exception(__name__, "_compute_isometric_fit_distance", e)
        tan_half_h = max(1e-4, tan_half_v * aspect)

        fit_v = half_h / tan_half_v
        fit_h = half_w / tan_half_h
        return max(float(min_distance), max(fit_v, fit_h) * max(1.01, float(margin_factor)))

    def focus_on_shelf_slot_isometric(
        self: "ModelViewWidget",
        rack_id: str | None,
        slot_id: str | None,
        shelf_index: int | None = None,
        zone_id: str | None = None,
    ) -> bool:
        """Переключает камеру в изометрический вид и фокусируется на выбранной секции полки."""
        if not self._plotter:
            return False
        rid = str(rack_id or "")
        sid = str(slot_id or "")
        zid = str(zone_id or "")
        if not rid or not sid:
            return False
        entry = self._get_rack_entry(rid, zid) if zid else self._get_rack_entry(rid)
        if entry is None:
            return False

        target_bounds = None
        requested_idx = int(1 if shelf_index is None else shelf_index)
        rack_type = str((entry.get("params") or {}).get("rack_type") or "").strip().upper()
        if rack_type == "PALLET":
            # tree-index=1 — напольный уровень под первой полкой,
            # tree-index=2..N+1 — физические полки (actor 1..N).
            use_floor_slot_bounds = requested_idx <= 1
            selected_idx = max(1, requested_idx - 1)
        else:
            use_floor_slot_bounds = requested_idx <= 0
            selected_idx = max(1, requested_idx)
        shelf_actors = list((self._rack_shelf_actors.get((rid, sid)) or []))
        if not use_floor_slot_bounds and shelf_actors and selected_idx <= len(shelf_actors):
            actor = shelf_actors[selected_idx - 1]
            if actor is not None:
                try:
                    target_bounds = actor.GetBounds()
                except Exception as e:
                    log_exception(__name__, "focus_on_shelf_slot_isometric", e)
                    target_bounds = None

        if not target_bounds:
            slot_actor = (entry.get("slot_actors") or {}).get(sid)
            if slot_actor is not None:
                try:
                    target_bounds = slot_actor.GetBounds()
                except Exception as e:
                    log_exception(__name__, "focus_on_shelf_slot_isometric", e)
                    target_bounds = None

        if not target_bounds:
            return False
        min_x, max_x, min_y, max_y, min_z, max_z = [float(v) for v in target_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)
        cz = min_z + (sz * 0.60)
        target_span = max(sx, sy, sz)

        cam = getattr(self._plotter, "camera", None)
        if cam is None:
            return False

        try:
            start_pos = tuple(float(v) for v in cam.GetPosition())
            start_foc = 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__, "focus_on_shelf_slot_isometric", e)
            return False

        try:
            # Вычисляем целевое изометрическое направление напрямую (без промежуточного прыжка камеры).
            # На 60% приближаем "изометрию полки" к прямому (front-like) виду:
            # уменьшаем боковой компонент yaw, сохраняя вертикальную составляющую.
            vx, vy, vz = 0.4, -1.0, 0.8
            norm = (vx * vx + vy * vy + vz * vz) ** 0.5
            if norm <= 1e-6:
                vx, vy, vz = 1.0, -1.0, 0.8
                norm = (vx * vx + vy * vy + vz * vz) ** 0.5
            vx /= norm
            vy /= norm
            vz /= norm
            ux, uy, uz = 0.0, 0.0, 1.0
            # right = forward x up (forward направлен от камеры к точке фокуса).
            fx, fy, fz = -vx, -vy, -vz
            rx = fy * uz - fz * uy
            ry = fz * ux - fx * uz
            rz = fx * uy - fy * ux
            right_norm = (rx * rx + ry * ry + rz * rz) ** 0.5
            if right_norm <= 1e-6:
                rx, ry, rz = 1.0, 0.0, 0.0
                right_norm = 1.0
            rx /= right_norm
            ry /= right_norm
            rz /= right_norm

            # Гарантируем полное попадание слота в кадр + небольшой зазор.
            distance = 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=1.09,
                min_distance=700.0,
            )
            target_focal = (cx, cy, cz)
            base_pos = (
                cx + vx * distance,
                cy + vy * distance,
                cz + vz * distance,
            )
            right_shift = target_span * 0.22
            down_shift = target_span * 0.06
            target_pos = (
                base_pos[0] + rx * right_shift - ux * down_shift,
                base_pos[1] + ry * right_shift - uy * down_shift,
                base_pos[2] + rz * right_shift + (sz * 0.08) - uz * down_shift,
            )
            target_focal = (
                target_focal[0] + rx * (target_span * 0.03),
                target_focal[1] + ry * (target_span * 0.03),
                target_focal[2] + (sz * 0.05),
            )

            # Анимируем переход камеры напрямую из текущей позиции в целевую изометрическую.
            self._animate_camera_transition(
                start_pos=start_pos,
                start_focal=start_foc,
                start_up=start_up,
                target_pos=target_pos,
                target_focal=target_focal,
                target_up=(ux, uy, uz),
                duration_ms=380,
                steps=20,
            )
            return True
        except Exception as e:
            log_exception(__name__, "focus_on_shelf_slot_isometric", e)
            return False

    def _animate_camera_transition(
        self: "ModelViewWidget",
        *,
        start_pos: tuple[float, float, float],
        start_focal: tuple[float, float, float],
        start_up: tuple[float, float, float],
        target_pos: tuple[float, float, float],
        target_focal: tuple[float, float, float],
        target_up: tuple[float, float, float],
        duration_ms: int = 320,
        steps: int = 16,
    ) -> None:
        """Анимирует переход камеры со сглаживанием smoothstep."""
        if not self._plotter or not getattr(self._plotter, "camera", None):
            return
        cam = self._plotter.camera
        total_steps = max(1, int(steps))
        interval_ms = max(10, int(duration_ms / total_steps))

        self._camera_anim_seq = int(getattr(self, "_camera_anim_seq", 0)) + 1
        anim_seq = self._camera_anim_seq
        old_timer = getattr(self, "_camera_anim_timer", None)
        if old_timer is not None:
            try:
                old_timer.stop()
            except Exception as e:
                log_exception(__name__, "_animate_camera_transition", e)
        timer = QTimer()
        self._camera_anim_timer = timer
        state = {"step": 0}

        def _lerp3(a: tuple[float, float, float], b: tuple[float, float, float], t: float) -> tuple[float, float, float]:
            return (
                float(a[0] + (b[0] - a[0]) * t),
                float(a[1] + (b[1] - a[1]) * t),
                float(a[2] + (b[2] - a[2]) * t),
            )

        def _tick() -> None:
            if anim_seq != getattr(self, "_camera_anim_seq", 0):
                timer.stop()
                return
            state["step"] = int(state["step"]) + 1
            linear_t = min(1.0, float(state["step"]) / float(total_steps))
            # smoothstep: плавное начало/конец без перелёта.
            t = linear_t * linear_t * (3.0 - 2.0 * linear_t)
            pos_t = _lerp3(start_pos, target_pos, t)
            focal_t = _lerp3(start_focal, target_focal, t)
            up_t = _lerp3(start_up, target_up, t)
            try:
                cam.SetPosition(*pos_t)
                cam.SetFocalPoint(*focal_t)
                if hasattr(cam, "SetViewUp"):
                    cam.SetViewUp(*up_t)
                if hasattr(self, "_reset_camera_clipping_range"):
                    self._reset_camera_clipping_range()
                self._plotter.update()
            except Exception as e:
                log_exception(__name__, "_animate_camera_transition._tick", e)
                timer.stop()
                return
            if linear_t >= 1.0:
                timer.stop()

        timer.timeout.connect(_tick)
        timer.start(interval_ms)


# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
# 1) Задача модуля:
#    Камера стеллажей: фокус на ячейке и плавная анимация перехода.
#
# 2) Последовательность действий и вызовов:
#    A. Класс RackCameraMixin: точки входа
#       Публичные методы сценария:
#       - RackCameraMixin.focus_on_shelf_slot_isometric(...)
#
#    B. RackCameraMixin: основной сценарий:
#       RackCameraMixin.focus_on_shelf_slot_isometric(...)
#         Назначение: Переключает камеру в изометрический вид и фокусируется на выбранной секции полки.
#         Последовательность внутренних вызовов:
#         -> RackCameraMixin._animate_camera_transition(...)
#
#    C. RackCameraMixin: вспомогательные расчёты:
#       RackCameraMixin._animate_camera_transition(...)
#         Назначение: Анимирует переход камеры со сглаживанием smoothstep.
#
# 3) Важные ограничения и инварианты:
#    - Модуль выполняется в составе ModelViewWidget и использует согласованные поля состояния self._... .
#    - Все операции с актёрами и камерой выполняются только при валидном self._plotter с безопасной обработкой исключений.
#    - Межмодульная связность: только через фасад model_view; прямые обращения между zone, rack, shelf, cell запрещены.
#    - Очистка состояния должна быть идемпотентной: повторный вызов не меняет корректное состояние в ошибочное.
#
# 4) Правило сопровождения:
#    - Любое изменение сценария должно сопровождаться обновлением этого блока с сохранением фактического порядка вызовов.
#    - При добавлении метода указывать его место в цепочке сценария (запуск, основной шаг, завершение, вспомогательная логика).
