import logging
import os

from contextlib import contextmanager

import bcad.binterpreter.boccviewer as OCCViewer

from bcad.binterpreter.singleton import Singleton
from bcad.binterpreter.rqq import replies, RP_ACK, RP_ACK_SET_SIZE, RP_ACK_GET_OBJECT_TREE, RP_NOP, RP_MEASURE_RESULT

import json
import math


@contextmanager
def _suppress_stdout_stderr():
    null_fd = os.open(os.devnull, os.O_WRONLY)
    old_out = os.dup(1)
    old_err = os.dup(2)
    os.dup2(null_fd, 1)
    os.dup2(null_fd, 2)
    os.close(null_fd)
    try:
        yield
    finally:
        os.dup2(old_out, 1)
        os.dup2(old_err, 2)
        os.close(old_out)
        os.close(old_err)

from OCC.Core.TopAbs import TopAbs_VERTEX
from OCC.Core.TopoDS import topods
from OCC.Core.BRep import BRep_Tool
from OCC.Core.Quantity import Quantity_Color, Quantity_NOC_GREEN, Quantity_NOC_YELLOW
from OCC.Core.Aspect import Aspect_TypeOfMarker
from OCC.Core.Prs3d import Prs3d_PointAspect, Prs3d_TypeOfHighlight

#from OCC.Core.AIS import AIS_Shape, AIS_Shaded, AIS_TexturedShape, AIS_WireFrame,AIS_SelectionModesConcurrency_Multiple

# Вставьте этот блок настроек в начало offscreen_display.py:

# ==========================================
# КОНФИГУРАЦИЯ УПРАВЛЕНИЯ ВЬЮПОРТОМ
# ==========================================
# Режим вращения модели. Допустимые значения:
# 'turntable' - поворотный стол (Z-ось жестко вертикальна, в стиле OpenSCAD)
# 'trackball' - свободный трекбол (по умолчанию в OpenCASCADE)
ROTATION_MODE = 'turntable'

# Коэффициенты чувствительности
ROTATE_SENSITIVITY = 0.005   # Чувствительность вращения (для режима 'turntable')
PAN_SENSITIVITY = 5.0        # Чувствительность сдвига сцены (панорамирования)

# True  = Камера плавно останавливается на полюсах (запрет переворота).
# False = Камера свободно перелетает через полюс (переворот вверх ногами).
LIMIT_POLES = False
# ==========================================

# True = Если при клике мыши камера перевернута, инвертировать ось X
# для интуитивного вращения до конца текущего перетаскивания.
INVERT_X_WHEN_UPSIDE_DOWN = True
# ==========================================

# Коэффициент увеличения на один «щелчок» колеса мыши.
# Используется и для scroll, и для smooth zoom (средняя кнопка).
ZOOM_FACTOR = 1.41
# ==========================================

class offscreenViewer3d:
    def __init__(self, *kargs):
        self.pt = (0,0)
        self.pipe = None
        self.img = None
        self.measure_mode = False
        self._has_result = False
        self._base_scale = 1.0

    def init_driver(self):
        if logging.getLogger().getEffectiveLevel() <= logging.DEBUG:
            self._display = OCCViewer.OffscreenRenderer()
        else:
            with _suppress_stdout_stderr():
                self._display = OCCViewer.OffscreenRenderer()

        bg1, bg2 = Singleton.settings.get_rgb_pair('display')
        self._display.set_bg_gradient_color(bg1, bg2)
        self._display.SetPerspectiveProjection()

        self._display.register_select_callback(self._on_select)
        self._setup_measure_styles()

        self._inited = True

    def _setup_measure_styles(self):
        ctx = self._display.Context

        for mode in [
            Prs3d_TypeOfHighlight.Prs3d_TypeOfHighlight_Dynamic,
            Prs3d_TypeOfHighlight.Prs3d_TypeOfHighlight_LocalDynamic,
        ]:
            h = ctx.HighlightStyle(mode)
            h.SetColor(Quantity_Color(Quantity_NOC_YELLOW))
            h.SetPointAspect(Prs3d_PointAspect(
                Aspect_TypeOfMarker.Aspect_TOM_O_STAR , Quantity_Color(Quantity_NOC_YELLOW), 4.0))

        for mode in [
            Prs3d_TypeOfHighlight.Prs3d_TypeOfHighlight_Selected,
            Prs3d_TypeOfHighlight.Prs3d_TypeOfHighlight_LocalSelected
        ]:
            h = ctx.HighlightStyle(mode)
            h.SetColor(Quantity_Color(Quantity_NOC_GREEN))
            h.SetPointAspect(Prs3d_PointAspect(
                Aspect_TypeOfMarker.Aspect_TOM_O_STAR, Quantity_Color(Quantity_NOC_GREEN), 2.0))

    def _on_select(self, shapes, x, y):
        if not self.measure_mode:
            return
        verts = [s for s in shapes if s.ShapeType() == TopAbs_VERTEX]
        if len(verts) >= 2:
            p1 = BRep_Tool.Pnt(topods.Vertex(verts[0]))
            p2 = BRep_Tool.Pnt(topods.Vertex(verts[1]))
            self.pipe.send(json.dumps({
                'rp': replies[RP_MEASURE_RESULT],
                'args': [f"Distance: {p1.Distance(p2):.3f} mm"]
            }))
            self._has_result = True

    def _turntable_rotate(self, dx, dy):
        """ Внутренний метод: математика вращения камеры через сферические координаты """
        from OCC.Core.gp import gp_Dir, gp_Pnt

        # --- Применяем множитель к оси X ---
        # Используем getattr для безопасности (по умолчанию 1.0)
        multiplier = getattr(self, 'drag_x_multiplier', 1.0)
        angle_x = (dx * multiplier) * ROTATE_SENSITIVITY
        angle_y = dy * ROTATE_SENSITIVITY
        # -------------------------------------------------

        camera = self._display.View.Camera()
        center = camera.Center()
        eye = camera.Eye()

        vx = eye.X() - center.X()
        vy = eye.Y() - center.Y()
        vz = eye.Z() - center.Z()

        r = math.sqrt(vx*vx + vy*vy + vz*vz)
        if r < 1e-6:
            return

        # Считываем "память" камеры: перевернуты ли мы в данный момент?
        is_upside_down = camera.Up().Z() < 0.0

        theta = math.atan2(vy, vx)
        phi_raw = math.asin(vz / r) # Даст значение строго от -pi/2 до pi/2

        # ВОССТАНОВЛЕНИЕ ИНФОРМАЦИИ: если мы вверх ногами, вычисляем истинные углы
        if is_upside_down:
            phi = math.pi - phi_raw
            theta = theta + math.pi
        else:
            phi = phi_raw

        # Применяем вращение от мыши
        theta -= angle_x
        phi += angle_y

        up_z = 1.0
        if LIMIT_POLES:
            limit = (math.pi / 2.0) - 0.001
            phi = max(-limit, min(limit, phi))
        else:
            # Если cos(phi) отрицательный, значит мы находимся "за полюсом"
            if math.cos(phi) < 0:
                up_z = -1.0

        new_vx = r * math.cos(phi) * math.cos(theta)
        new_vy = r * math.cos(phi) * math.sin(theta)
        new_vz = r * math.sin(phi)

        camera.SetEye(gp_Pnt(center.X() + new_vx, center.Y() + new_vy, center.Z() + new_vz))
        camera.SetUp(gp_Dir(0, 0, up_z))


    def set_pipe(self, pipe):
        self.pipe = pipe

    def set_img(self, img):
        self.img = img

    def repaint(self):
        self._display.Repaint()

    def make_current(self):
        self._display.MakeCurrent()

    def swap_buffers(self):
        self._display.SwapBuffers()

    def update_img(self):
        if hasattr(Singleton, 'sd') and Singleton.sd is not None:
            Singleton.sd._update_axis_length()
        w = getattr(self, 'w', 1280)
        h = getattr(self, 'h', 1024)
        data = self._display.GetImageData(w, h, 1)
        self.img.buf[:len(data)] = data

    def call_select(self, x, y):
        if self.measure_mode and self._has_result:
            self._display.ClearSelected()
            self._has_result = False
        self._display.MoveTo(int(x), int(y))
        self._display.ShiftSelect(int(x), int(y))
        self.update_img()
        self.pipe.send(json.dumps({'rp': replies[RP_ACK]}))

    def call_measure_clear(self):
        if self.measure_mode:
            self._display.ClearSelected()
        self._has_result = False
        self.update_img()
        self.pipe.send(json.dumps({'rp': replies[RP_ACK]}))

    def call_set_measure_mode(self, on):
        self.measure_mode = on
        if on:
            self._display.SetSelectionModeVertex()
        else:
            self._display.SetSelectionModeShape()
            self._display.ClearSelected()
            self._has_result = False
        self.update_img()
        self.pipe.send(json.dumps({'rp': replies[RP_ACK]}))

    def call_start_rotation(self, x, y):

        if ROTATION_MODE == 'trackball':
            self._display.StartRotation(int(x), int(y))
            self.update_img()
        else: # 'turntable'
            # Запоминаем стартовые координаты для вычисления дельт движения
            self.prev_rotate_x = x
            self.prev_rotate_y = y

            # Фиксируем множитель X-оси на весь драг ---
            self.drag_x_multiplier = 1.0
            if INVERT_X_WHEN_UPSIDE_DOWN:
                camera = self._display.View.Camera()
                # Если вектор "Вверх" смотрит вниз, значит мы перевернуты
                if camera.Up().Z() < 0.0:
                    self.drag_x_multiplier = -1.0

        self.pipe.send(json.dumps({'rp': replies[RP_ACK]}))

    def call_rotate(self, x, y):
        """ Главный обработчик вращения """
        if ROTATION_MODE == 'trackball':
            self._display.Rotation(int(x), int(y))
        else: # 'turntable'
            dx = x - self.prev_rotate_x
            dy = y - self.prev_rotate_y

            self.prev_rotate_x = x
            self.prev_rotate_y = y

            self._turntable_rotate(dx, dy)

        # Общие действия вынесены вниз (выполняются в любом режиме)
        self._display.Repaint()
        self.update_img()
        self.pipe.send(json.dumps({'rp': replies[RP_ACK]}))

    def call_set_size(self, w, h):
        self.w = w
        self.h = h
        self._display.SetSize(w, h)
        self.repaint()
        self.update_img()
        self.pipe.send(json.dumps({'rp': replies[RP_ACK_SET_SIZE], 'args': [w, h]}))

    def call_load_image(self):
        self.repaint()
        self.update_img()

    def call_scroll(self, delta):
        # delta — величина прокрутки (обычно +1.0 или -1.0, но может быть дробной)

        f = abs(delta)*ZOOM_FACTOR
        if delta < 0:
          f = 1/f

        self._display.ZoomFactor(f)
        self.update_img()
        self.pipe.send(json.dumps({'rp': replies[RP_ACK]}))

    def call_move(self, x, y):
        self._display.MoveTo(int(x), int(y))
        self.repaint()
        self.update_img()
        self.pipe.send(json.dumps({'rp': replies[RP_ACK]}))

    def call_start_zoom(self):
        self._base_scale = self._display.View.Scale()
        self.pipe.send(json.dumps({'rp': replies[RP_ACK]}))

    def call_zoom(self, dy):
        N = 200.0
        scale = self._base_scale * (1.0 + (ZOOM_FACTOR-1) * dy / N)
        self._display.SetScale(scale)
        self._display.Repaint()
        self.update_img()
        self.pipe.send(json.dumps({'rp': replies[RP_ACK]}))

    def call_pan(self, x, y):

        dx = int(x * PAN_SENSITIVITY)
        dy = int(-y * PAN_SENSITIVITY)

        self._display.Pan(dx, dy)
        self._display.Repaint()
        self.update_img()
        self.pipe.send(json.dumps({'rp': replies[RP_ACK]}))

    def call_check_redraw(self):
        if Singleton.should_redraw:
            Singleton.should_redraw = False
            self.update_img()
            self.pipe.send(json.dumps({'rp': replies[RP_ACK]}))
        else:
            self.pipe.send(json.dumps({'rp': replies[RP_NOP]}))

    def call_get_object_tree(self):
        self.pipe.send(json.dumps({'rp': replies[RP_ACK_GET_OBJECT_TREE], 'args': Singleton.scl.objects}))

    def call_set_display_state(self, shaded, show_edges):
        self._display.SetDisplayState(shaded, show_edges)
        self._display.Repaint()
        self.update_img()
        self.pipe.send(json.dumps({'rp': replies[RP_ACK]}))

    def call_set_projection(self, mode):
        if mode == 'orthographic':
            self._display.SetOrthographicProjection()
        elif mode == 'perspective':
            self._display.SetPerspectiveProjection()
        self.update_img()
        self.pipe.send(json.dumps({'rp': replies[RP_ACK]}))

    def call_view_action(self, action, *args):
        if action == 'reset_view':
            self._display.ResetView()
        elif action == 'fit_all':
            self._display.FitAll()
        elif action == 'triedron_show':
            self._display.display_triedron()
        elif action == 'triedron_hide':
            self._display.hide_triedron()
        elif action == 'export_image':
            self._display.ExportToImage(args[0])
        self._display.Repaint()
        self.update_img()
        self.pipe.send(json.dumps({'rp': replies[RP_ACK]}))

    def call_end_interaction(self):
        self.update_img()
        self.pipe.send(json.dumps({'rp': replies[RP_ACK]}))

    def call_set_view(self, name):
        if name == 'left':
            self._display.View_Left()
        elif name == 'right':
            self._display.View_Right()
        elif name == 'top':
            self._display.View_Top()
        elif name == 'bottom':
            self._display.View_Bottom()
        elif name == 'front':
            self._display.View_Front()
        elif name == 'rear':
            self._display.View_Rear()
        elif name == 'iso1':
            self._display.View_Iso1()
        elif name == 'iso2':
            self._display.View_Iso2()
        elif name == 'iso3':
            self._display.View_Iso3()
        elif name == 'iso4':
            self._display.View_Iso4()
        elif name == 'iso5':
            self._display.View_Iso5()
        elif name == 'iso6':
            self._display.View_Iso6()
        elif name == 'iso7':
            self._display.View_Iso7()
        elif name == 'iso8':
            self._display.View_Iso8()
        self.update_img()
        self.pipe.send(json.dumps({'rp': replies[RP_ACK]}))
