# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2020 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.WindowFlags()):
            # 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

            if qt.BINDING in ('PyQt5', 'PySide2'):
                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.WindowFlags()):
        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)

        else:
            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.
        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 not None:
            # TODO check if this is correct on different OS/screen
            # OK on macOS10.12/qt5.13.2
            dpi = screen.physicalDotsPerInch() * self.getDevicePixelRatio()
        else:  # Fallback
            dpi = 96. * self.getDevicePixelRatio()
        return dpi

    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
