# -*- coding: utf-8 -*-

# Copyright (c) 2008 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
#


"""
Module implementing the helpbrowser using QWebView.
"""

from __future__ import unicode_literals, print_function
try:
    str = unicode       # __IGNORE_EXCEPTION__
except NameError:
    pass

from PyQt5.QtCore import pyqtSlot, pyqtSignal, QUrl, QUrlQuery, QTimer, \
    QEventLoop, QPoint, QPointF
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineSettings, \
    QWebEngineScript
from PyQt5.QtWebChannel import QWebChannel

from E5Gui import E5MessageBox

from WebBrowser.WebBrowserWindow import WebBrowserWindow

from .JavaScript.ExternalJsObject import ExternalJsObject

from .Tools.WebHitTestResult import WebHitTestResult
from .Tools import Scripts

import Preferences
from Globals import qVersionTuple


class WebBrowserPage(QWebEnginePage):
    """
    Class implementing an enhanced web page.
    
    @signal safeBrowsingAbort() emitted to indicate an abort due to a safe
        browsing event
    @signal safeBrowsingBad(threatType, threatMessages) emitted to indicate a
        malicious web site as determined by safe browsing
    @signal printPageRequested() emitted to indicate a print request of the
        shown web page
    @signal navigationRequestAccepted(url, navigation type, main frame) emitted
        to signal an accepted navigation request
    """
    if qVersionTuple() >= (5, 7, 0):
        SafeJsWorld = QWebEngineScript.ApplicationWorld
        # SafeJsWorld = QWebEngineScript.MainWorld
    else:
        SafeJsWorld = QWebEngineScript.MainWorld
    UnsafeJsWorld = QWebEngineScript.MainWorld
    
    safeBrowsingAbort = pyqtSignal()
    safeBrowsingBad = pyqtSignal(str, str)
    
    printPageRequested = pyqtSignal()
    navigationRequestAccepted = pyqtSignal(QUrl, QWebEnginePage.NavigationType,
                                           bool)
    
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent parent widget of this window (QWidget)
        """
        super(WebBrowserPage, self).__init__(
            WebBrowserWindow.webProfile(), parent)
        
        self.__printer = None
        self.__badSite = False
        self.__registerProtocolHandlerRequest = None
        
        self.featurePermissionRequested.connect(
            self.__featurePermissionRequested)
        self.authenticationRequired.connect(
            lambda url, auth: WebBrowserWindow.networkManager().authentication(
                url, auth, self))
        self.proxyAuthenticationRequired.connect(
            WebBrowserWindow.networkManager().proxyAuthentication)
        self.fullScreenRequested.connect(self.__fullScreenRequested)
        self.urlChanged.connect(self.__urlChanged)
        
        try:
            self.contentsSizeChanged.connect(self.__contentsSizeChanged)
        except AttributeError:
            # defined for Qt >= 5.7
            pass
        
        try:
            self.registerProtocolHandlerRequested.connect(
                self.__registerProtocolHandlerRequested)
        except AttributeError:
            # defined for Qt >= 5.11
            pass
        
        # Workaround for changing webchannel world inside
        # acceptNavigationRequest not working
        self.__channelUrl = QUrl()
        self.__channelWorldId = -1
        self.__setupChannelTimer = QTimer(self)
        self.__setupChannelTimer.setSingleShot(True)
        self.__setupChannelTimer.setInterval(100)
        self.__setupChannelTimer.timeout.connect(self.__setupChannelTimeout)
    
    @pyqtSlot()
    def __setupChannelTimeout(self):
        """
        Private slot to initiate the setup of the web channel.
        """
        self.__setupWebChannelForUrl(self.__channelUrl)
    
    def acceptNavigationRequest(self, url, type_, isMainFrame):
        """
        Public method to determine, if a request may be accepted.
        
        @param url URL to navigate to
        @type QUrl
        @param type_ type of the navigation request
        @type QWebEnginePage.NavigationType
        @param isMainFrame flag indicating, that the request originated from
            the main frame
        @type bool
        @return flag indicating acceptance
        @rtype bool
        """
        scheme = url.scheme()
        if scheme == "mailto":
            QDesktopServices.openUrl(url)
            return False
        
        # AdBlock
        if url.scheme() == "abp":
            if WebBrowserWindow.adBlockManager().addSubscriptionFromUrl(url):
                return False
        
        # GreaseMonkey
        if type_ == QWebEnginePage.NavigationTypeLinkClicked and \
           url.toString().endswith(".user.js"):
            WebBrowserWindow.greaseMonkeyManager().downloadScript(url)
            return False
        
        if url.scheme() == "eric":
            if url.path() == "AddSearchProvider":
                query = QUrlQuery(url)
                self.view().mainWindow().openSearchManager().addEngine(
                    QUrl(query.queryItemValue("url")))
                return False
            elif url.path() == "PrintPage":
                self.printPageRequested.emit()
                return False
        
        # Safe Browsing
        self.__badSite = False
        from WebBrowser.SafeBrowsing.SafeBrowsingManager import \
            SafeBrowsingManager
        if SafeBrowsingManager.isEnabled() and \
            url.scheme() not in \
                SafeBrowsingManager.getIgnoreSchemes():
            threatLists = \
                WebBrowserWindow.safeBrowsingManager().lookupUrl(url)[0]
            if threatLists:
                threatMessages = WebBrowserWindow.safeBrowsingManager()\
                    .getThreatMessages(threatLists)
                res = E5MessageBox.warning(
                    WebBrowserWindow.getWindow(),
                    self.tr("Suspicuous URL detected"),
                    self.tr("<p>The URL <b>{0}</b> was found in the Safe"
                            " Browsing database.</p>{1}").format(
                        url.toString(), "".join(threatMessages)),
                    E5MessageBox.StandardButtons(
                        E5MessageBox.Abort |
                        E5MessageBox.Ignore),
                    E5MessageBox.Abort)
                if res == E5MessageBox.Abort:
                    self.safeBrowsingAbort.emit()
                    return False
                
                self.__badSite = True
                threatType = WebBrowserWindow.safeBrowsingManager()\
                    .getThreatType(threatLists[0])
                self.safeBrowsingBad.emit(threatType, "".join(threatMessages))
        
        result = QWebEnginePage.acceptNavigationRequest(self, url, type_,
                                                        isMainFrame)
        
        if result:
            if isMainFrame:
                isWeb = url.scheme() in ("http", "https", "ftp", "ftps",
                                         "file")
                globalJsEnabled = WebBrowserWindow.webSettings().testAttribute(
                    QWebEngineSettings.JavascriptEnabled)
                if isWeb:
                    enable = globalJsEnabled
                else:
                    enable = True
                self.settings().setAttribute(
                    QWebEngineSettings.JavascriptEnabled, enable)
                
                self.__channelUrl = url
                self.__setupChannelTimer.start()
            self.navigationRequestAccepted.emit(url, type_, isMainFrame)
        
        return result
    
    @pyqtSlot(QUrl)
    def __urlChanged(self, url):
        """
        Private slot to handle changes of the URL.
        
        @param url new URL
        @type QUrl
        """
        if not url.isEmpty() and url.scheme() == "eric" and \
                not self.isJavaScriptEnabled():
            self.settings().setAttribute(QWebEngineSettings.JavascriptEnabled,
                                         True)
            self.triggerAction(QWebEnginePage.Reload)
    
    @classmethod
    def userAgent(cls, resolveEmpty=False):
        """
        Class method to get the global user agent setting.
        
        @param resolveEmpty flag indicating to resolve an empty
            user agent (boolean)
        @return user agent string (string)
        """
        agent = Preferences.getWebBrowser("UserAgent")
        if agent == "" and resolveEmpty:
            agent = cls.userAgentForUrl(QUrl())
        return agent
    
    @classmethod
    def setUserAgent(cls, agent):
        """
        Class method to set the global user agent string.
        
        @param agent new current user agent string (string)
        """
        Preferences.setWebBrowser("UserAgent", agent)
    
    @classmethod
    def userAgentForUrl(cls, url):
        """
        Class method to determine the user agent for the given URL.
        
        @param url URL to determine user agent for (QUrl)
        @return user agent string (string)
        """
        agent = WebBrowserWindow.userAgentsManager().userAgentForUrl(url)
        if agent == "":
            # no agent string specified for the given host -> use global one
            agent = Preferences.getWebBrowser("UserAgent")
            if agent == "":
                # no global agent string specified -> use default one
                agent = WebBrowserWindow.webProfile().httpUserAgent()
        return agent
    
    def __featurePermissionRequested(self, url, feature):
        """
        Private slot handling a feature permission request.
        
        @param url url requesting the feature
        @type QUrl
        @param feature requested feature
        @type QWebEnginePage.Feature
        """
        manager = WebBrowserWindow.featurePermissionManager()
        manager.requestFeaturePermission(self, url, feature)
    
    def execJavaScript(self, script, worldId=QWebEngineScript.MainWorld,
                       timeout=500):
        """
        Public method to execute a JavaScript function synchroneously.
        
        @param script JavaScript script source to be executed
        @type str
        @param worldId ID to run the script under
        @type int
        @param timeout max. time the script is given to execute
        @type int
        @return result of the script
        @rtype depending upon script result
        """
        loop = QEventLoop()
        resultDict = {"res": None}
        QTimer.singleShot(timeout, loop.quit)
        
        def resultCallback(res, resDict=resultDict):
            if loop and loop.isRunning():
                resDict["res"] = res
                loop.quit()
        
        self.runJavaScript(script, worldId, resultCallback)
        
        loop.exec_()
        return resultDict["res"]
    
    def runJavaScript(self, script, worldId=-1, callback=None):
        """
        Public method to run a script in the context of the page.
        
        @param script JavaScript script source to be executed
        @type str
        @param worldId ID to run the script under
        @type int
        @param callback callback function to be executed when the script has
            ended
        @type function
        """
        if qVersionTuple() >= (5, 7, 0) and worldId > -1:
            if callback is None:
                QWebEnginePage.runJavaScript(self, script, worldId)
            else:
                QWebEnginePage.runJavaScript(self, script, worldId, callback)
        else:
            if callback is None:
                QWebEnginePage.runJavaScript(self, script)
            else:
                QWebEnginePage.runJavaScript(self, script, callback)
    
    def isJavaScriptEnabled(self):
        """
        Public method to test, if JavaScript is enabled.
        
        @return flag indicating the state of the JavaScript support
        @rtype bool
        """
        return self.settings().testAttribute(
            QWebEngineSettings.JavascriptEnabled)
    
    def scroll(self, x, y):
        """
        Public method to scroll by the given amount of pixels.
        
        @param x horizontal scroll value
        @type int
        @param y vertical scroll value
        @type int
        """
        self.runJavaScript(
            "window.scrollTo(window.scrollX + {0}, window.scrollY + {1})"
            .format(x, y),
            WebBrowserPage.SafeJsWorld
        )
    
    def scrollTo(self, pos):
        """
        Public method to scroll to the given position.
        
        @param pos position to scroll to
        @type QPointF
        """
        self.runJavaScript(
            "window.scrollTo({0}, {1});".format(pos.x(), pos.y()),
            WebBrowserPage.SafeJsWorld
        )
    
    def mapToViewport(self, pos):
        """
        Public method to map a position to the viewport.
        
        @param pos position to be mapped
        @type QPoint
        @return viewport position
        @rtype QPoint
        """
        return QPoint(pos.x() // self.zoomFactor(),
                      pos.y() // self.zoomFactor())
    
    def hitTestContent(self, pos):
        """
        Public method to test the content at a specified position.
        
        @param pos position to execute the test at
        @type QPoint
        @return test result object
        @rtype WebHitTestResult
        """
        return WebHitTestResult(self, pos)
    
    def __setupWebChannelForUrl(self, url):
        """
        Private method to setup a web channel to our external object.
        
        @param url URL for which to setup the web channel
        @type QUrl
        """
        channel = self.webChannel()
        if channel is None:
            channel = QWebChannel(self)
            ExternalJsObject.setupWebChannel(channel, self)
        
        worldId = -1
        if url.scheme() in ("eric", "qthelp"):
            worldId = self.UnsafeJsWorld
        else:
            worldId = self.SafeJsWorld
        if worldId != self.__channelWorldId:
            self.__channelWorldId = worldId
            try:
                self.setWebChannel(channel, self.__channelWorldId)
            except TypeError:
                # pre Qt 5.7.0
                self.setWebChannel(channel)
    
    def certificateError(self, error):
        """
        Public method to handle SSL certificate errors.
        
        @param error object containing the certificate error information
        @type QWebEngineCertificateError
        @return flag indicating to ignore this error
        @rtype bool
        """
        return WebBrowserWindow.networkManager().certificateError(
            error, self.view())
    
    def __fullScreenRequested(self, request):
        """
        Private slot handling a full screen request.
        
        @param request reference to the full screen request
        @type QWebEngineFullScreenRequest
        """
        self.view().requestFullScreen(request.toggleOn())
        
        accepted = request.toggleOn() == self.view().isFullScreen()
        
        if accepted:
            request.accept()
        else:
            request.reject()
    
    def execPrintPage(self, printer, timeout=1000):
        """
        Public method to execute a synchronous print.
        
        @param printer reference to the printer object
        @type QPrinter
        @param timeout timeout value in milliseconds
        @type int
        @return flag indicating a successful print job
        @rtype bool
        """
        loop = QEventLoop()
        resultDict = {"res": None}
        QTimer.singleShot(timeout, loop.quit)
        
        def printCallback(res, resDict=resultDict):
            if loop and loop.isRunning():
                resDict["res"] = res
                loop.quit()
        
        self.print(printer, printCallback)
        
        loop.exec_()
        return resultDict["res"]
    
    def __contentsSizeChanged(self, size):
        """
        Private slot to work around QWebEnginePage not scrolling to anchors
        when opened in a background tab.
        
        @param size changed contents size (unused)
        @type QSize
        """
        fragment = self.url().fragment()
        self.runJavaScript(Scripts.scrollToAnchor(fragment))
    
    ##############################################
    ## Methods below deal with JavaScript messages
    ##############################################
    
    def javaScriptConsoleMessage(self, level, message, lineNumber, sourceId):
        """
        Public method to show a console message.
        
        @param level severity
        @type QWebEnginePage.JavaScriptConsoleMessageLevel
        @param message message to be shown
        @type str
        @param lineNumber line number of an error
        @type int
        @param sourceId source URL causing the error
        @type str
        """
        self.view().mainWindow().javascriptConsole().javaScriptConsoleMessage(
            level, message, lineNumber, sourceId)
    
    ###########################################################################
    ## Methods below implement safe browsing related functions
    ###########################################################################
    
    def getSafeBrowsingStatus(self):
        """
        Public method to get the safe browsing status of the current page.
        
        @return flag indicating a safe site
        @rtype bool
        """
        return not self.__badSite
    
    ##################################################
    ## Methods below implement compatibility functions
    ##################################################
    
    if not hasattr(QWebEnginePage, "icon"):
        def icon(self):
            """
            Public method to get the web site icon.
            
            @return web site icon
            @rtype QIcon
            """
            return self.view().icon()
    
    if not hasattr(QWebEnginePage, "scrollPosition"):
        def scrollPosition(self):
            """
            Public method to get the scroll position of the web page.
            
            @return scroll position
            @rtype QPointF
            """
            pos = self.execJavaScript(
                "(function() {"
                "var res = {"
                "    x: 0,"
                "    y: 0,"
                "};"
                "res.x = window.scrollX;"
                "res.y = window.scrollY;"
                "return res;"
                "})()",
                WebBrowserPage.SafeJsWorld
            )
            if pos is not None:
                pos = QPointF(pos["x"], pos["y"])
            else:
                pos = QPointF(0.0, 0.0)
            
            return pos
    
    #############################################################
    ## Methods below implement protocol handler related functions
    #############################################################
    
    try:
        @pyqtSlot("QWebEngineRegisterProtocolHandlerRequest")
        def __registerProtocolHandlerRequested(self, request):
            """
            Private slot to handle the registration of a custom protocol
            handler.
            
            @param request reference to the registration request
            @type QWebEngineRegisterProtocolHandlerRequest
            """
            from PyQt5.QtWebEngineCore import \
                QWebEngineRegisterProtocolHandlerRequest
            
            if self.__registerProtocolHandlerRequest:
                del self.__registerProtocolHandlerRequest
                self.__registerProtocolHandlerRequest = None
            self.__registerProtocolHandlerRequest = \
                QWebEngineRegisterProtocolHandlerRequest(request)
    except TypeError:
        # this is supported with Qt 5.12 and later
        pass
    
    def registerProtocolHandlerRequestUrl(self):
        """
        Public method to get the registered protocol handler request URL.
        
        @return registered protocol handler request URL
        @rtype QUrl
        """
        if self.__registerProtocolHandlerRequest and \
            (self.url().host() ==
             self.__registerProtocolHandlerRequest.origin().host()):
            return self.__registerProtocolHandlerRequest.origin()
        else:
            return QUrl()
    
    def registerProtocolHandlerRequestScheme(self):
        """
        Public method to get the registered protocol handler request scheme.
        
        @return registered protocol handler request scheme
        @rtype str
        """
        if self.__registerProtocolHandlerRequest and \
            (self.url().host() ==
             self.__registerProtocolHandlerRequest.origin().host()):
            return self.__registerProtocolHandlerRequest.scheme()
        else:
            return ""
