"""Qt OpenGL viewport and live ModernGL host."""

from __future__ import annotations

from contextlib import contextmanager
import logging
from typing import Any

import moderngl as mgl
from PIL import Image
from PySide6 import QtCore, QtGui
from PySide6.QtOpenGLWidgets import QOpenGLWidget

from ...mgl.target import OwnedRenderTarget, RenderTarget
from .theme import ThemeSpec, theme_spec_for_mode

logger = logging.getLogger(__name__)


class QtOpenGLLoader:
    """ModernGL loader backed by the current Qt OpenGL context."""

    def __init__(self, context: QtGui.QOpenGLContext):
        if context is None:
            raise RuntimeError("Qt OpenGL context is not available.")
        self._context = context

    def load_opengl_function(self, name: str) -> int:
        proc = self._context.getProcAddress(str(name))
        if proc is None:
            return 0
        try:
            return int(proc)
        except TypeError:
            return 0

    def __enter__(self):
        return self

    def __exit__(self, *args):
        return None

    def release(self):
        return None


class QtViewportHost:
    """Live viewer host backed by a :class:`QOpenGLWidget`."""

    def __init__(self, viewport: "QtViewport"):
        self._viewport = viewport

    @property
    def scene(self) -> Any:
        return self._viewport._scene

    @property
    def size(self) -> tuple[int, int]:
        return self._viewport.current_render_size()

    def _context_description(self) -> str:
        current = QtGui.QOpenGLContext.currentContext()
        widget = self._viewport.context()
        return (
            f"current_context_present={current is not None} "
            f"widget_context_present={widget is not None} "
            f"current_matches_widget={bool(current is not None and widget is not None and current == widget)}"
        )

    def _make_current(self, reason: str) -> None:
        logger.debug("makeCurrent begin reason=%s %s", reason, self._context_description())
        self._viewport.makeCurrent()
        logger.debug("makeCurrent end reason=%s %s", reason, self._context_description())

    def _done_current(self, reason: str) -> None:
        logger.debug("doneCurrent begin reason=%s %s", reason, self._context_description())
        self._viewport.doneCurrent()
        logger.debug("doneCurrent end reason=%s %s", reason, self._context_description())

    def create_scene(self, scene_cls: Any, system: Any) -> Any:
        if self._viewport._ctx is None:
            raise RuntimeError("OpenGL context is not initialized yet.")
        logger.debug(
            "Creating scene scene_cls=%s system_size=%s %s",
            getattr(scene_cls, "__name__", repr(scene_cls)),
            tuple(int(v) for v in getattr(system, "size", ())),
            self._context_description(),
        )
        self._make_current("create_scene")
        try:
            scene = scene_cls(mglctx=self._viewport._ctx, system=system)
        except Exception:
            logger.exception(
                "Failed to create scene scene_cls=%s",
                getattr(scene_cls, "__name__", repr(scene_cls)),
            )
            raise
        finally:
            self._done_current("create_scene")
        logger.debug(
            "Created scene scene_cls=%s scene_type=%s",
            getattr(scene_cls, "__name__", repr(scene_cls)),
            type(scene).__name__,
        )
        return scene

    def attach_scene(self, scene: Any) -> Any:
        logger.debug(
            "Attaching scene scene_type=%s %s",
            type(scene).__name__,
            self._context_description(),
        )
        self._viewport._scene = scene
        return self

    def upload(self, state) -> Any:
        if self._viewport._scene is None:
            raise RuntimeError("No scene is attached.")
        logger.debug(
            "Uploading state scene_type=%s shape=%s dtype=%s %s",
            type(self._viewport._scene).__name__,
            tuple(int(v) for v in getattr(state, "shape", ())),
            getattr(state, "dtype", None),
            self._context_description(),
        )
        self._make_current("upload")
        try:
            self._viewport._scene.upload(state)
        except Exception:
            logger.exception(
                "Failed to upload state into scene scene_type=%s",
                type(self._viewport._scene).__name__,
            )
            raise
        finally:
            self._done_current("upload")
        return self

    @contextmanager
    def scene_mutation_scope(self, reason: str):
        self._make_current(reason)
        try:
            yield self
        finally:
            self._done_current(reason)

    def render_image(
        self,
        *,
        size: tuple[int, int] | None = None,
    ) -> Image.Image:
        if self._viewport._ctx is None or self._viewport._scene is None:
            raise RuntimeError("Viewport is not ready to render.")

        requested = (
            self._viewport.current_render_size()
            if size is None
            else (int(size[0]), int(size[1]))
        )
        logger.debug(
            "Rendering image size=%s scene_type=%s %s",
            requested,
            type(self._viewport._scene).__name__,
            self._context_description(),
        )
        self._make_current("render_image")
        target = OwnedRenderTarget(self._viewport._ctx, requested)
        try:
            self._viewport._prepare_render_target("render_image", target=target)
            self._viewport._scene.render_to(target)
            self._viewport._ctx.finish()
            data = target.read_rgb()
        finally:
            target.release()
            self._done_current("render_image")
        image = Image.frombytes("RGB", requested, data)
        return image.transpose(Image.FLIP_TOP_BOTTOM)


class QtViewport(QOpenGLWidget):
    """Central live viewport for the desktop viewer."""

    host_ready = QtCore.Signal()
    session_state_changed = QtCore.Signal(bool)
    runtime_error = QtCore.Signal(str)

    def __init__(self, parent: QtCore.QObject | None = None):
        super().__init__(parent)
        self._ctx: mgl.Context | None = None
        self._scene = None
        self._session = None
        self._mouse_pointer: tuple[float, float] | None = None
        self._drag_changed = False
        self._paint_error_signature: tuple[str, str] | None = None
        app = QtGui.QGuiApplication.instance()
        self._theme_spec = theme_spec_for_mode("system", app) if app is not None else None
        self.host = QtViewportHost(self)
        self.setAttribute(QtCore.Qt.WidgetAttribute.WA_OpaquePaintEvent, True)
        self.setAutoFillBackground(False)
        self.setMouseTracking(True)
        self.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus)
        self.setUpdateBehavior(QOpenGLWidget.UpdateBehavior.NoPartialUpdate)

    @staticmethod
    def create_surface_format() -> QtGui.QSurfaceFormat:
        fmt = QtGui.QSurfaceFormat()
        fmt.setRenderableType(QtGui.QSurfaceFormat.RenderableType.OpenGL)
        fmt.setVersion(3, 3)
        fmt.setProfile(QtGui.QSurfaceFormat.OpenGLContextProfile.CoreProfile)
        fmt.setSwapInterval(1)
        fmt.setDepthBufferSize(24)
        fmt.setSamples(4)
        return fmt

    def current_render_size(self) -> tuple[int, int]:
        dpr = max(float(self.devicePixelRatioF()), 1.0)
        return (
            max(1, int(round(self.width() * dpr))),
            max(1, int(round(self.height() * dpr))),
        )

    @staticmethod
    def _enum_name(enum_value) -> str:
        try:
            return str(enum_value).rsplit(".", 1)[-1]
        except Exception:
            return repr(enum_value)

    @classmethod
    def _surface_format_snapshot(cls, fmt: QtGui.QSurfaceFormat | None) -> dict[str, object] | None:
        if fmt is None:
            return None
        return {
            "version": (int(fmt.majorVersion()), int(fmt.minorVersion())),
            "profile": cls._enum_name(fmt.profile()),
            "renderable_type": cls._enum_name(fmt.renderableType()),
            "swap_interval": int(fmt.swapInterval()),
            "samples": int(fmt.samples()),
            "depth_buffer_size": int(fmt.depthBufferSize()),
            "stencil_buffer_size": int(fmt.stencilBufferSize()),
            "alpha_buffer_size": int(fmt.alphaBufferSize()),
        }

    def _runtime_diagnostics(self, qt_context: QtGui.QOpenGLContext | None) -> dict[str, object]:
        info = dict(getattr(self._ctx, "info", {}) or {})
        actual_format = None
        is_opengles = None
        if qt_context is not None:
            format_getter = getattr(qt_context, "format", None)
            if callable(format_getter):
                actual_format = format_getter()
            gles_getter = getattr(qt_context, "isOpenGLES", None)
            if callable(gles_getter):
                try:
                    is_opengles = bool(gles_getter())
                except Exception:
                    is_opengles = None
        return {
            "vendor": info.get("GL_VENDOR"),
            "renderer": info.get("GL_RENDERER"),
            "version": info.get("GL_VERSION"),
            "glsl": info.get("GL_SHADING_LANGUAGE_VERSION"),
            "version_code": getattr(self._ctx, "version_code", None),
            "default_framebuffer": int(self.defaultFramebufferObject() or 0),
            "is_opengles": is_opengles,
            "requested_format": self._surface_format_snapshot(self.format()),
            "actual_format": self._surface_format_snapshot(actual_format),
        }

    @property
    def is_ready(self) -> bool:
        return self._ctx is not None

    def _current_widget_target(self) -> RenderTarget:
        if self._ctx is None:
            raise RuntimeError("OpenGL context is not initialized yet.")
        framebuffer_id = int(self.defaultFramebufferObject())
        if framebuffer_id <= 0:
            raise RuntimeError("QOpenGLWidget framebuffer is not initialized yet.")
        framebuffer = self._ctx.detect_framebuffer(framebuffer_id)
        framebuffer_size = tuple(
            int(v) for v in getattr(framebuffer, "size", self.current_render_size())
        )
        return RenderTarget(ctx=self._ctx, fbo=framebuffer, size=framebuffer_size)

    def _prepare_render_target(self, reason: str, *, target: RenderTarget) -> None:
        if self._ctx is None:
            raise RuntimeError("OpenGL context framebuffer is not initialized yet.")
        target.bind()
        blend_func = mgl.DEFAULT_BLENDING
        blend_equation = mgl.FUNC_ADD
        self._ctx.scissor = None
        target.fbo.scissor = None
        target.fbo.viewport = (0, 0, *target.size)
        target.fbo.color_mask = (True, True, True, True)
        target.fbo.depth_mask = True
        self._ctx.enable_only(mgl.NOTHING)
        self._ctx.blend_func = blend_func
        self._ctx.blend_equation = blend_equation
        self._ctx.viewport = (0, 0, *target.size)
        logger.debug(
            "Prepared render target reason=%s framebuffer=%s render_size=%s ctx_scissor=%s fbo_scissor=%s fbo_viewport=%s color_mask=%s depth_mask=%s blend_func=%s blend_equation=%s",
            reason,
            getattr(target.fbo, "glo", None),
            target.size,
            getattr(self._ctx, "scissor", None),
            getattr(target.fbo, "scissor", None),
            getattr(target.fbo, "viewport", None),
            getattr(target.fbo, "color_mask", None),
            getattr(target.fbo, "depth_mask", None),
            blend_func,
            blend_equation,
        )

    def set_session(self, session) -> None:
        self._session = session

    def apply_theme(self, spec: ThemeSpec | None) -> None:
        self._theme_spec = spec
        self.update()

    def _empty_clear_color(self) -> tuple[float, float, float, float]:
        if self._theme_spec is not None:
            return self._theme_spec.viewport_clear_rgba
        return (1.0, 1.0, 1.0, 1.0)

    def clear_session(self) -> None:
        self._session = None
        self._scene = None
        self.update()

    def shutdown(self) -> None:
        if self._ctx is None:
            self._session = None
            self._scene = None
            return
        logger.info("Shutting down topovec view OpenGL viewport")
        try:
            self.makeCurrent()
            self._session = None
            self._scene = None
            try:
                self._ctx.gc()
            except Exception:
                logger.debug("ModernGL context gc failed during shutdown.", exc_info=True)
            self._ctx.release()
        except Exception:
            logger.exception("Failed while shutting down topovec view OpenGL viewport")
        finally:
            self._ctx = None
            try:
                self.doneCurrent()
            except Exception:
                logger.debug("doneCurrent failed during viewport shutdown.", exc_info=True)

    def initializeGL(self) -> None:
        qt_context = QtGui.QOpenGLContext.currentContext() or self.context()
        logger.debug(
            "initializeGL begin current_context_present=%s widget_context_present=%s render_size=%s dpr=%.3f",
            QtGui.QOpenGLContext.currentContext() is not None,
            self.context() is not None,
            self.current_render_size(),
            max(float(self.devicePixelRatioF()), 1.0),
        )
        try:
            loader = QtOpenGLLoader(qt_context)
            logger.debug("Initializing ModernGL from Qt OpenGL context via getProcAddress()")
            mgl.init_context(loader=loader)
            self._ctx = mgl.get_context()
            logger.debug("ModernGL context initialized through Qt loader")
        except Exception:
            logger.exception("Qt-based ModernGL initialization failed; falling back to default loader")
            try:
                self._ctx = mgl.create_context()
            except Exception:
                logger.exception("Failed to initialize ModernGL context for topovec view viewport")
                self.runtime_error.emit(
                    "Failed to initialize the OpenGL viewport. "
                    "Qt OpenGL context initialization failed and moderngl fallback "
                    "could not load system OpenGL libraries. See Log for details."
                )
                self._ctx = None
                return
        if self._ctx is None:
            self.runtime_error.emit("Failed to initialize the OpenGL viewport. See Log for details.")
            return
        diagnostics = self._runtime_diagnostics(qt_context)
        logger.info(
            "OpenGL viewport initialized render_size=%s dpr=%.3f vendor=%s renderer=%s version=%s glsl=%s version_code=%s default_framebuffer=%s is_opengles=%s actual_format=%s requested_format=%s",
            self.current_render_size(),
            max(float(self.devicePixelRatioF()), 1.0),
            diagnostics["vendor"],
            diagnostics["renderer"],
            diagnostics["version"],
            diagnostics["glsl"],
            diagnostics["version_code"],
            diagnostics["default_framebuffer"],
            diagnostics["is_opengles"],
            diagnostics["actual_format"],
            diagnostics["requested_format"],
        )
        logger.debug(
            "initializeGL end current_context_present=%s moderngl_ctx_present=%s",
            QtGui.QOpenGLContext.currentContext() is not None,
            self._ctx is not None,
        )
        self.host_ready.emit()

    def paintGL(self) -> None:
        if self._ctx is None:
            return
        try:
            target = self._current_widget_target()
            self._prepare_render_target("paintGL", target=target)
            if self._scene is None:
                target.clear_color(*self._empty_clear_color())
                self._ctx.finish()
                self._paint_error_signature = None
                return
            logger.debug(
                "paintGL rendering scene_type=%s framebuffer=%s render_size=%s",
                type(self._scene).__name__,
                getattr(target.fbo, "glo", None),
                target.size,
            )
            self._scene.render_to(target)
            self._ctx.finish()
            self._paint_error_signature = None
        except Exception as exc:
            signature = (type(exc).__name__, str(exc))
            if signature != self._paint_error_signature:
                logger.exception(
                    "Viewport paint failed scene_type=%s render_size=%s",
                    type(self._scene).__name__ if self._scene is not None else None,
                    self.current_render_size(),
                )
                self.runtime_error.emit("Viewport rendering failed. See Log for details.")
                self._paint_error_signature = signature
            try:
                target = self._current_widget_target()
                self._prepare_render_target("paintGL-error", target=target)
                target.clear_color(*self._empty_clear_color())
                self._ctx.finish()
            except Exception:
                logger.debug("Viewport clear after paint failure also failed.", exc_info=True)

    def _event_point(self, event) -> tuple[float, float]:
        scale = max(self.height() / 2.0, 1.0)
        return event.position().x() / scale, event.position().y() / scale

    def mousePressEvent(self, event) -> None:
        self._mouse_pointer = self._event_point(event)
        self._drag_changed = False
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event) -> None:
        if self._session is None:
            return super().mouseMoveEvent(event)
        buttons = event.buttons()
        if not buttons:
            self._mouse_pointer = None
            self._drag_changed = False
            return super().mouseMoveEvent(event)

        point = self._event_point(event)
        if self._mouse_pointer is None:
            self._mouse_pointer = point
            return super().mouseMoveEvent(event)

        dx = point[0] - self._mouse_pointer[0]
        dy = point[1] - self._mouse_pointer[1]
        modifiers = event.modifiers()
        if buttons & QtCore.Qt.MouseButton.RightButton or (
            modifiers & QtCore.Qt.KeyboardModifier.ShiftModifier
        ):
            event_type = "pan"
        else:
            event_type = "rotate"
        changed = self._session.apply_camera_delta(
            event_type=event_type,
            dx=dx,
            dy=dy,
        )
        self._mouse_pointer = point
        if changed:
            self._drag_changed = True
            self.update()
            self.session_state_changed.emit(False)
        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event) -> None:
        if self._session is not None and self._drag_changed:
            changed = self._session.commit_camera_interaction()
            self.update()
            self.session_state_changed.emit(bool(changed))
        self._mouse_pointer = None
        self._drag_changed = False
        super().mouseReleaseEvent(event)

    def wheelEvent(self, event) -> None:
        if self._session is None:
            return super().wheelEvent(event)
        delta = event.angleDelta()
        amount = delta.y() if abs(delta.y()) >= abs(delta.x()) else delta.x()
        if not amount:
            return super().wheelEvent(event)
        if event.modifiers() & QtCore.Qt.KeyboardModifier.ShiftModifier:
            changed = self._session.apply_camera_delta(
                event_type="roll",
                dx=float(amount) / 120.0,
            )
        else:
            changed = self._session.apply_camera_delta(
                event_type="zoom",
                wheel=float(amount) / 120.0,
            )
        if changed:
            self._session.commit_camera_interaction()
            self.update()
            self.session_state_changed.emit(True)
        super().wheelEvent(event)

    def apply_camera_button(self, action: str) -> bool:
        if self._session is None:
            return False
        changed = self._session.apply_camera_button(action)
        if changed:
            self.update()
            self.session_state_changed.emit(True)
        return changed


QtViewportWindow = QtViewport


__all__ = [
    "QtOpenGLLoader",
    "QtViewport",
    "QtViewportHost",
    "QtViewportWindow",
]
