# /*##########################################################################
#
# Copyright (c) 2017-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ###########################################################################*/
"""This package provides a compatibility layer for OpenGL widget.

It provides a compatibility layer for Qt OpenGL widget used in silx
across Qt<=5.3 QtOpenGL.QGLWidget and QOpenGLWidget.
"""

__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "22/11/2019"


import logging
import sys

from .. import qt
from ..utils.glutils import isOpenGLAvailable
from .._glutils import gl


_logger = logging.getLogger(__name__)


if not hasattr(qt, "QOpenGLWidget") and not hasattr(qt, "QGLWidget"):
    _OpenGLWidget = None

else:
    if hasattr(qt, "QOpenGLWidget"):  # PyQt>=5.4
        _logger.info("Using QOpenGLWidget")
        _BaseOpenGLWidget = qt.QOpenGLWidget

    else:
        _logger.info("Using QGLWidget")
        _BaseOpenGLWidget = qt.QGLWidget

    class _OpenGLWidget(_BaseOpenGLWidget):
        """Wrapper over QOpenGLWidget and QGLWidget"""

        sigOpenGLContextError = qt.Signal(str)
        """Signal emitted when an OpenGL context error is detected at runtime.

        It provides the error reason as a str.
        """

        def __init__(
            self,
            parent,
            alphaBufferSize=0,
            depthBufferSize=24,
            stencilBufferSize=8,
            version=(2, 0),
            f=qt.Qt.Widget,
        ):
            # True if using QGLWidget, False if using QOpenGLWidget
            self.__legacy = not hasattr(qt, "QOpenGLWidget")

            self.__devicePixelRatio = 1.0
            self.__requestedOpenGLVersion = int(version[0]), int(version[1])
            self.__isValid = False

            if self.__legacy:  # QGLWidget
                format_ = qt.QGLFormat()
                format_.setAlphaBufferSize(alphaBufferSize)
                format_.setAlpha(alphaBufferSize != 0)
                format_.setDepthBufferSize(depthBufferSize)
                format_.setDepth(depthBufferSize != 0)
                format_.setStencilBufferSize(stencilBufferSize)
                format_.setStencil(stencilBufferSize != 0)
                format_.setVersion(*self.__requestedOpenGLVersion)
                format_.setDoubleBuffer(True)

                super(_OpenGLWidget, self).__init__(format_, parent, None, f)

            else:  # QOpenGLWidget
                super(_OpenGLWidget, self).__init__(parent, f)

                format_ = qt.QSurfaceFormat()
                format_.setAlphaBufferSize(alphaBufferSize)
                format_.setDepthBufferSize(depthBufferSize)
                format_.setStencilBufferSize(stencilBufferSize)
                format_.setVersion(*self.__requestedOpenGLVersion)
                format_.setSwapBehavior(qt.QSurfaceFormat.DoubleBuffer)
                self.setFormat(format_)

            # Enable receiving mouse move events when no buttons are pressed
            self.setMouseTracking(True)

        def getDevicePixelRatio(self):
            """Returns the ratio device-independent / device pixel size

            It should be either 1.0 or 2.0.

            :return: Scale factor between screen and Qt units
            :rtype: float
            """
            return self.__devicePixelRatio

        def getRequestedOpenGLVersion(self):
            """Returns the requested OpenGL version.

            :return: (major, minor)
            :rtype: 2-tuple of int"""
            return self.__requestedOpenGLVersion

        def getOpenGLVersion(self):
            """Returns the available OpenGL version.

            :return: (major, minor)
            :rtype: 2-tuple of int"""
            if self.__legacy:  # QGLWidget
                supportedVersion = 0, 0

                # Go through all OpenGL version flags checking support
                flags = self.format().openGLVersionFlags()
                for version in (
                    (1, 1),
                    (1, 2),
                    (1, 3),
                    (1, 4),
                    (1, 5),
                    (2, 0),
                    (2, 1),
                    (3, 0),
                    (3, 1),
                    (3, 2),
                    (3, 3),
                    (4, 0),
                ):
                    versionFlag = getattr(
                        qt.QGLFormat, "OpenGL_Version_%d_%d" % version
                    )
                    if not versionFlag & flags:
                        break
                    supportedVersion = version
                return supportedVersion

            else:  # QOpenGLWidget
                return self.format().version()

        # QOpenGLWidget methods

        def isValid(self):
            """Returns True if OpenGL is available.

            This adds extra checks to Qt isValid method.

            :rtype: bool
            """
            return self.__isValid and super(_OpenGLWidget, self).isValid()

        def defaultFramebufferObject(self):
            """Returns the framebuffer object handle.

            See :meth:`QOpenGLWidget.defaultFramebufferObject`
            """
            if self.__legacy:  # QGLWidget
                return 0
            else:  # QOpenGLWidget
                return super(_OpenGLWidget, self).defaultFramebufferObject()

        # *GL overridden methods

        def initializeGL(self):
            parent = self.parent()
            if parent is None:
                _logger.error("_OpenGLWidget has no parent")
                return

            # Check OpenGL version
            if self.getOpenGLVersion() >= self.getRequestedOpenGLVersion():
                try:
                    gl.glGetError()  # clear any previous error (if any)
                    version = gl.glGetString(gl.GL_VERSION)
                except:
                    version = None

                if version:
                    self.__isValid = True
                else:
                    errMsg = "OpenGL not available"
                    if sys.platform.startswith("linux"):
                        errMsg += (
                            ": If connected remotely, "
                            "GLX forwarding might be disabled."
                        )
                    _logger.error(errMsg)
                    self.sigOpenGLContextError.emit(errMsg)
                    self.__isValid = False

            else:
                errMsg = "OpenGL %d.%d not available" % self.getRequestedOpenGLVersion()
                _logger.error("OpenGL widget disabled: %s", errMsg)
                self.sigOpenGLContextError.emit(errMsg)
                self.__isValid = False

            if self.isValid():
                parent.initializeGL()

        def paintGL(self):
            parent = self.parent()
            if parent is None:
                _logger.error("_OpenGLWidget has no parent")
                return

            devicePixelRatio = self.window().windowHandle().devicePixelRatio()

            if devicePixelRatio != self.getDevicePixelRatio():
                # Update devicePixelRatio and call resizeOpenGL
                # as resizeGL is not always called.
                self.__devicePixelRatio = devicePixelRatio
                self.makeCurrent()
                parent.resizeGL(self.width(), self.height())

            if self.isValid():
                parent.paintGL()

        def resizeGL(self, width, height):
            parent = self.parent()
            if parent is None:
                _logger.error("_OpenGLWidget has no parent")
                return

            if self.isValid():
                # Call parent resizeGL with device-independent pixel unit
                # This works over both QGLWidget and QOpenGLWidget
                parent.resizeGL(self.width(), self.height())


class OpenGLWidget(qt.QWidget):
    """OpenGL widget wrapper over QGLWidget and QOpenGLWidget

    This wrapper API implements a subset of QOpenGLWidget API.
    The constructor takes a different set of arguments.
    Methods returning object like :meth:`context` returns either
    QGL* or QOpenGL* objects.

    :param parent: Parent widget see :class:`QWidget`
    :param int alphaBufferSize:
        Size in bits of the alpha channel (default: 0).
        Set to 0 to disable alpha channel.
    :param int depthBufferSize:
        Size in bits of the depth buffer (default: 24).
        Set to 0 to disable depth buffer.
    :param int stencilBufferSize:
        Size in bits of the stencil buffer (default: 8).
        Set to 0 to disable stencil buffer
    :param version: Requested OpenGL version (default: (2, 0)).
    :type version: 2-tuple of int
    :param f: see :class:`QWidget`
    """

    def __init__(
        self,
        parent=None,
        alphaBufferSize=0,
        depthBufferSize=24,
        stencilBufferSize=8,
        version=(2, 0),
        f=qt.Qt.Widget,
    ):
        super(OpenGLWidget, self).__init__(parent, f)

        layout = qt.QHBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)

        self.__context = None

        _check = isOpenGLAvailable(version=version, runtimeCheck=False)
        if _OpenGLWidget is None or not _check:
            _logger.error("OpenGL-based widget disabled: %s", _check.error)
            self.__openGLWidget = None
            label = self._createErrorQLabel(_check.error)
            self.layout().addWidget(label)
            return

        self.__openGLWidget = _OpenGLWidget(
            parent=self,
            alphaBufferSize=alphaBufferSize,
            depthBufferSize=depthBufferSize,
            stencilBufferSize=stencilBufferSize,
            version=version,
            f=f,
        )
        # Async connection need, otherwise issue when hiding OpenGL
        # widget while doing the rendering..
        self.__openGLWidget.sigOpenGLContextError.connect(
            self._handleOpenGLInitError, qt.Qt.QueuedConnection
        )
        self.layout().addWidget(self.__openGLWidget)

    @staticmethod
    def _createErrorQLabel(error):
        """Create QLabel displaying error message in place of OpenGL widget

        :param str error: The error message to display"""
        label = qt.QLabel()
        label.setText("OpenGL-based widget disabled:\n%s" % error)
        label.setAlignment(qt.Qt.AlignCenter)
        label.setWordWrap(True)
        return label

    def _handleOpenGLInitError(self, error):
        """Handle runtime errors in OpenGL widget"""
        if self.__openGLWidget is not None:
            self.__openGLWidget.setVisible(False)
            self.__openGLWidget.setParent(None)
            self.__openGLWidget = None

            label = self._createErrorQLabel(error)
            self.layout().addWidget(label)

    # Additional API

    def getDevicePixelRatio(self):
        """Returns the ratio device-independent / device pixel size

        It should be either 1.0 or 2.0.

        :return: Scale factor between screen and Qt units
        :rtype: float
        """
        if self.__openGLWidget is None:
            return 1.0
        else:
            return self.__openGLWidget.getDevicePixelRatio()

    def getDotsPerInch(self):
        """Returns current screen resolution as device pixels per inch.

        :rtype: float
        """
        screen = self.window().windowHandle().screen()
        if screen is None:
            return 96.0 * self.getDevicePixelRatio()

        physicalDPI = screen.physicalDotsPerInch()
        if physicalDPI > 1000.0:
            _logger.error(
                "Reported screen DPI too high: %f, using default value instead",
                physicalDPI,
            )
            physicalDPI = 96.0
        return physicalDPI * self.getDevicePixelRatio()

    def getOpenGLVersion(self):
        """Returns the available OpenGL version.

        :return: (major, minor)
        :rtype: 2-tuple of int"""
        if self.__openGLWidget is None:
            return 0, 0
        else:
            return self.__openGLWidget.getOpenGLVersion()

    # QOpenGLWidget API

    def isValid(self):
        """Returns True if OpenGL with the requested version is available.

        :rtype: bool
        """
        if self.__openGLWidget is None:
            return False
        else:
            return self.__openGLWidget.isValid()

    def context(self):
        """Return Qt OpenGL context object or None.

        See :meth:`QOpenGLWidget.context` and :meth:`QGLWidget.context`
        """
        if self.__openGLWidget is None:
            return None
        else:
            # Keep a reference on QOpenGLContext to make
            # else PyQt5 keeps creating a new one.
            self.__context = self.__openGLWidget.context()
            return self.__context

    def defaultFramebufferObject(self):
        """Returns the framebuffer object handle.

        See :meth:`QOpenGLWidget.defaultFramebufferObject`
        """
        if self.__openGLWidget is None:
            return 0
        else:
            return self.__openGLWidget.defaultFramebufferObject()

    def makeCurrent(self):
        """Make the underlying OpenGL widget's context current.

        See :meth:`QOpenGLWidget.makeCurrent`
        """
        if self.__openGLWidget is not None:
            self.__openGLWidget.makeCurrent()

    def update(self):
        """Async update of the OpenGL widget.

        See :meth:`QOpenGLWidget.update`
        """
        if self.__openGLWidget is not None:
            self.__openGLWidget.update()

    # QOpenGLWidget API to override

    def initializeGL(self):
        """Override to implement OpenGL initialization."""
        pass

    def paintGL(self):
        """Override to implement OpenGL rendering."""
        pass

    def resizeGL(self, width, height):
        """Override to implement resize of OpenGL framebuffer.

        :param int width: Width in device-independent pixels
        :param int height: Height in device-independent pixels
        """
        pass
