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

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

"""
Module implementing a widget to show some source code information provided by
plug-ins.
"""

from __future__ import unicode_literals
try:
    basestring    # __IGNORE_WARNING__
except NameError:
    basestring = str

from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QTimer
from PyQt5.QtGui import QCursor
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, \
    QComboBox, QSizePolicy, QLineEdit, QTextEdit, QToolTip, QToolButton, \
    QActionGroup, QMenu

from E5Gui.E5TextEditSearchWidget import E5TextEditSearchWidget
from E5Gui.E5ToolButton import E5ToolButton

import Preferences
import UI.PixmapCache

from .CodeDocumentationViewerTemplate import \
    prepareDocumentationViewerHtmlDocument, \
    prepareDocumentationViewerHtmlDocWarningDocument, \
    prepareDocumentationViewerHtmlWarningDocument

from .data import codeDocumentationViewer_rc    # __IGNORE_WARNING__


class PlainTextDocumentationViewer(QWidget):
    """
    Class implementing the plain text documentation viewer.
    """
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent reference to the parent widget
        @type QWidget
        """
        super(PlainTextDocumentationViewer, self).__init__(parent)
        self.setObjectName("PlainTextDocumentationViewer")
        
        self.__verticalLayout = QVBoxLayout(self)
        self.__verticalLayout.setObjectName("verticalLayout")
        self.__verticalLayout.setContentsMargins(0, 0, 0, 0)
        
        self.__contents = QTextEdit(self)
        self.__contents.setTabChangesFocus(True)
        self.__contents.setReadOnly(True)
        self.__contents.setLineWrapMode(QTextEdit.NoWrap)
        self.__contents.setObjectName("contents")
        self.__verticalLayout.addWidget(self.__contents)
        
        self.__searchWidget = E5TextEditSearchWidget(self, False)
        self.__searchWidget.setFocusPolicy(Qt.WheelFocus)
        self.__searchWidget.setObjectName("searchWidget")
        self.__verticalLayout.addWidget(self.__searchWidget)
        
        self.__searchWidget.attachTextEdit(self.__contents, "QTextEdit")
        
        self.preferencesChanged()
    
    def clear(self):
        """
        Public method to clear the contents.
        """
        self.__contents.clear()
    
    def setText(self, text):
        """
        Public method to set the text to be shown.
        
        @param text text to be shown
        @type str
        """
        self.__contents.setPlainText(text)
    
    def preferencesChanged(self):
        """
        Public slot to handle a change of preferences.
        """
        font = Preferences.getEditorOtherFonts("MonospacedFont")
        self.__contents.setFontFamily(font.family())
        self.__contents.setFontPointSize(font.pointSize())


class WebViewDocumentationViewer(QWidget):
    """
    Class implementing the rich text documentation viewer.
    """
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent reference to the parent widget
        @type QWidget
        """
        super(WebViewDocumentationViewer, self).__init__(parent)
        self.setObjectName("WebViewDocumentationViewer")
        
        self.__verticalLayout = QVBoxLayout(self)
        self.__verticalLayout.setObjectName("verticalLayout")
        self.__verticalLayout.setContentsMargins(0, 0, 0, 0)
        
        try:
            from PyQt5.QtWebEngineWidgets import QWebEngineView, \
                QWebEngineSettings
            self.__contents = QWebEngineView(self)
            self.__contents.page().linkHovered.connect(self.__showLink)
            try:
                self.__contents.settings().setAttribute(
                    QWebEngineSettings.FocusOnNavigationEnabled, False)
            except AttributeError:
                # pre Qt 5.8
                pass
            self.__usesWebKit = False
        except ImportError:
            from PyQt5.QtWebKitWidgets import QWebPage, QWebView
            self.__contents = QWebView(self)
            self.__contents.page().setLinkDelegationPolicy(
                QWebPage.DelegateAllLinks)
            self.__usesWebKit = True
        
        sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.__contents.sizePolicy().hasHeightForWidth())
        self.__contents.setSizePolicy(sizePolicy)
        self.__contents.setContextMenuPolicy(Qt.NoContextMenu)
        self.__contents.setUrl(QUrl("about:blank"))
        self.__verticalLayout.addWidget(self.__contents)
        
        self.__searchWidget = E5TextEditSearchWidget(self, False)
        self.__searchWidget.setFocusPolicy(Qt.WheelFocus)
        self.__searchWidget.setObjectName("searchWidget")
        self.__verticalLayout.addWidget(self.__searchWidget)
        
        self.__searchWidget.attachTextEdit(
            self.__contents,
            "QWebView" if self.__usesWebKit else "QWebEngineView",
        )
    
    @pyqtSlot(str)
    def __showLink(self, urlStr):
        """
        Private slot to show the hovered link in a tooltip.
        
        @param urlStr hovered URL
        @type str
        """
        QToolTip.showText(QCursor.pos(), urlStr, self.__contents)
    
    def setHtml(self, html):
        """
        Public method to set the HTML text of the widget.
        
        @param html HTML text to be shown
        @type str
        """
        self.__contents.setEnabled(False)
        self.__contents.setHtml(html)
        self.__contents.setEnabled(True)
    
    def clear(self):
        """
        Public method to clear the shown contents.
        """
        self.__contents.setHtml("")

    
class CodeDocumentationViewer(QWidget):
    """
    Class implementing a widget to show some source code information provided
    by plug-ins.
    
    @signal providerAdded() emitted to indicate the availability of a new
        provider
    @signal providerRemoved() emitted to indicate the removal of a provider
    """
    providerAdded = pyqtSignal()
    providerRemoved = pyqtSignal()
    
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent reference to the parent widget
        @type QWidget
        """
        super(CodeDocumentationViewer, self).__init__(parent)
        self.__setupUi()
        
        self.__ui = parent
        
        self.__providers = {}
        self.__selectedProvider = ""
        self.__disabledProvider = "disabled"
        
        self.__shuttingDown = False
        self.__startingUp = True
        
        self.__lastDocumentation = None
        self.__requestingEditor = None
        
        self.__unregisterTimer = QTimer(self)
        self.__unregisterTimer.setInterval(30000)   # 30 seconds
        self.__unregisterTimer.setSingleShot(True)
        self.__unregisterTimer.timeout.connect(self.__unregisterTimerTimeout)
        self.__mostRecentlyUnregisteredProvider = None
    
    def __setupUi(self):
        """
        Private method to generate the UI layout.
        """
        self.setObjectName("CodeDocumentationViewer")
        
        self.verticalLayout = QVBoxLayout(self)
        self.verticalLayout.setObjectName("verticalLayout")
        self.verticalLayout.setContentsMargins(3, 3, 3, 3)
        
        # top row 1 of widgets
        self.horizontalLayout1 = QHBoxLayout()
        self.horizontalLayout1.setObjectName("horizontalLayout1")
        
        self.label = QLabel(self)
        self.label.setObjectName("label")
        self.label.setText(self.tr("Code Info Provider:"))
        self.label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
        self.horizontalLayout1.addWidget(self.label)
        
        self.providerComboBox = QComboBox(self)
        sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(
            self.providerComboBox.sizePolicy().hasHeightForWidth())
        self.providerComboBox.setSizePolicy(sizePolicy)
        self.providerComboBox.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self.providerComboBox.setObjectName("providerComboBox")
        self.providerComboBox.setToolTip(
            self.tr("Select the code info provider"))
        self.providerComboBox.addItem(self.tr("<disabled>"), "disabled")
        self.horizontalLayout1.addWidget(self.providerComboBox)
        
        # top row 2 of widgets
        self.horizontalLayout2 = QHBoxLayout()
        self.horizontalLayout2.setObjectName("horizontalLayout2")
        
        self.objectLineEdit = QLineEdit(self)
        self.objectLineEdit.setReadOnly(True)
        self.objectLineEdit.setObjectName("objectLineEdit")
        self.horizontalLayout2.addWidget(self.objectLineEdit)
        
        self.verticalLayout.addLayout(self.horizontalLayout1)
        self.verticalLayout.addLayout(self.horizontalLayout2)
        
        # Plain Text Viewer
        self.__plainTextViewer = PlainTextDocumentationViewer(self)
        self.__plainTextViewer.setObjectName("__plainTextViewer")
        self.verticalLayout.addWidget(self.__plainTextViewer)
        
        # Rich Text (Web) Viewer
        try:
            self.__richTextViewer = WebViewDocumentationViewer(self)
            self.__richTextViewer.setObjectName("__richTextViewer")
            self.verticalLayout.addWidget(self.__richTextViewer)
        except ImportError:
            # neither QtWebEngineWidgets nor QtWebKitWidgets is available
            self.__richTextViewer = None
        
        self.__toolButton = E5ToolButton(self)
        self.__toolButton.setObjectName(
            "navigation_supermenu_button")
        self.__toolButton.setIcon(UI.PixmapCache.getIcon("superMenu.png"))
        self.__toolButton.setToolTip(self.tr("Main Menu"))
        self.__toolButton.setPopupMode(QToolButton.InstantPopup)
        self.__toolButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
        self.__toolButton.setFocusPolicy(Qt.NoFocus)
        self.__toolButton.setAutoRaise(True)
        self.__toolButton.setShowMenuInside(True)
        
        self.__optionsActionGroup = QActionGroup(self)
        self.__optionsActionGroup.setExclusive(True)
        self.__optionsMenu = QMenu(self)
        if self.__richTextViewer:
            self.__richTextAct = self.__optionsMenu.addAction(
                self.tr("Rich Text"),
                lambda: self.__showTextViewer(True))
            self.__richTextAct.setCheckable(True)
            self.__optionsActionGroup.addAction(self.__richTextAct)
        else:
            # neither QtWebEngineWidgets nor QtWebKitWidgets is available
            self.__richTextAct = None
        self.__plainTextAct = self.__optionsMenu.addAction(
            self.tr("Plain Text"),
            lambda: self.__showTextViewer(False))
        self.__plainTextAct.setCheckable(True)
        self.__optionsActionGroup.addAction(self.__plainTextAct)
        
        self.__toolButton.setMenu(self.__optionsMenu)
        self.horizontalLayout2.addWidget(self.__toolButton)
        
        self.providerComboBox.currentIndexChanged[int].connect(
            self.on_providerComboBox_currentIndexChanged)
    
    def finalizeSetup(self):
        """
        Public method to finalize the setup of the documentation viewer.
        """
        self.__showTextViewer(
            Preferences.getDocuViewer("ShowInfoAsRichText"))
        
        self.__startingUp = False
        provider = Preferences.getDocuViewer("Provider")
        if provider in self.__providers:
            index = self.providerComboBox.findData(provider)
        else:
            index = 0
            provider = self.__disabledProvider
        self.providerComboBox.setCurrentIndex(index)
        self.__selectedProvider = provider
        if index == 0:
            self.__showDisabledMessage()
    
    def registerProvider(self, providerName, providerDisplay, provider,
                         supported):
        """
        Public method register a source docu provider.
        
        @param providerName name of the provider (must be unique)
        @type str
        @param providerDisplay visible name of the provider
        @type str
        @param provider function to be called to determine source docu
        @type function(editor)
        @param supported function to be called to determine, if a language is
            supported
        @type function(language)
        @exception KeyError raised if a provider with the given name was
            already registered
        """
        if providerName in self.__providers:
            raise KeyError(
                "Provider '{0}' already registered.".format(providerName))
        
        self.__providers[providerName] = (provider, supported)
        self.providerComboBox.addItem(providerDisplay, providerName)
        
        self.providerAdded.emit()
        
        if self.__unregisterTimer.isActive():
            if providerName == self.__mostRecentlyUnregisteredProvider:
                # this is assumed to be a plug-in reload
                self.__unregisterTimer.stop()
                self.__mostRecentlyUnregisteredProvider = None
                self.__selectProvider(providerName)
    
    def unregisterProvider(self, providerName):
        """
        Public method register a source docu provider.
        
        @param providerName name of the provider (must be unique)
        @type str
        """
        if providerName in self.__providers:
            if providerName == self.__selectedProvider:
                self.providerComboBox.setCurrentIndex(0)
                
                # in case this is just a temporary unregistration (< 30s)
                # e.g. when the plug-in is re-installed or updated
                self.__mostRecentlyUnregisteredProvider = providerName
                self.__unregisterTimer.start()
            
            del self.__providers[providerName]
            index = self.providerComboBox.findData(providerName)
            self.providerComboBox.removeItem(index)
            
            self.providerRemoved.emit()
    
    @pyqtSlot()
    def __unregisterTimerTimeout(self):
        """
        Private slot handling the timeout signal of the unregister timer.
        """
        self.__mostRecentlyUnregisteredProvider = None
    
    def isSupportedLanguage(self, language):
        """
        Public method to check, if the given language is supported by the
        selected provider.
        
        @param language editor programming language to check
        @type str
        @return flag indicating the support status
        @rtype bool
        """
        supported = False
        
        if self.__selectedProvider != self.__disabledProvider:
            supported = self.__providers[self.__selectedProvider][1](language)
        
        return supported
    
    def getProviders(self):
        """
        Public method to get a list of providers and their visible strings.
        
        @return list containing the providers and their visible strings
        @rtype list of tuple of (str,str)
        """
        providers = []
        for index in range(1, self.providerComboBox.count()):
            provider = self.providerComboBox.itemData(index)
            text = self.providerComboBox.itemText(index)
            providers.append((provider, text))
        
        return providers
    
    def showInfo(self, editor):
        """
        Public method to request code documentation data from a provider.
        
        @param editor reference to the editor to request code docu for
        @type Editor
        """
        line, index = editor.getCursorPosition()
        word = editor.getWord(line, index)
        if not word:
            # try again one index before
            word = editor.getWord(line, index - 1)
        self.objectLineEdit.setText(word)
        
        if self.__selectedProvider != self.__disabledProvider:
            self.__plainTextViewer.clear()
            if self.__richTextViewer:
                self.__richTextViewer.clear()
            self.__providers[self.__selectedProvider][0](editor)
    
    def documentationReady(self, documentationInfo, isWarning=False,
                           isDocWarning=False):
        """
        Public method to provide the documentation info to the viewer.
        
        If documentationInfo is a dictionary, it should contain these
        (optional) keys and data:
        
        name: the name of the inspected object
        argspec: its arguments specification
        note: A phrase describing the type of object (function or method) and
            the module it belongs to.
        docstring: its documentation string
        typ: its type information
        
        @param documentationInfo dictionary containing the source docu data
        @type dict or str
        @param isWarning flag indicating a warning page
        @type bool
        @param isDocWarning flag indicating a documentation warning page
        @type bool
        """
        self.__ui.activateCodeDocumentationViewer(switchFocus=False)
        
        if not isWarning and not isDocWarning:
            self.__lastDocumentation = documentationInfo
        
        if not documentationInfo:
            if self.__selectedProvider == self.__disabledProvider:
                self.__showDisabledMessage()
            else:
                self.documentationReady(self.tr("No documentation available"),
                                        isDocWarning=True)
        else:
            if self.__showMarkdown:
                if isWarning:
                    html = prepareDocumentationViewerHtmlWarningDocument(
                        documentationInfo)
                elif isDocWarning:
                    html = prepareDocumentationViewerHtmlDocWarningDocument(
                        documentationInfo)
                elif isinstance(documentationInfo, dict):
                    html = prepareDocumentationViewerHtmlDocument(
                        documentationInfo)
                else:
                    html = documentationInfo
                self.__setHtml(html)
            else:
                if isinstance(documentationInfo, basestring):
                    fullText = documentationInfo
                elif isinstance(documentationInfo, dict):
                    name = documentationInfo["name"]
                    if name:
                        title = "".join([name, "\n",
                                         "=" * len(name), "\n\n"])

                        if "argspec" in documentationInfo and \
                           documentationInfo["argspec"]:
                            definition = self.tr("Definition: {0}{1}\n")\
                                .format(name, documentationInfo["argspec"])
                        elif name:
                            definition = self.tr("Definition: {0}\n")\
                                .format(name)
                        else:
                            definition = ""
                        
                        if "typ" in documentationInfo and \
                           documentationInfo["typ"]:
                            typeInfo = self.tr("Type: {0}\n").format(
                                documentationInfo["typ"])
                        else:
                            typeInfo = ""
                        
                        if "note" in documentationInfo and \
                           documentationInfo["note"]:
                            note = self.tr("Note: {0}\n").format(
                                documentationInfo["note"])
                        else:
                            note = ""
                        
                        header = "".join([title, definition, typeInfo, note])
                    else:
                        header = ""
                    
                    if "docstring" not in documentationInfo or \
                       not documentationInfo["docstring"]:
                        docString = self.tr(
                            "No further documentation available")
                    else:
                        if header:
                            docString = "\n----\n\n{0}".format(
                                documentationInfo["docstring"])
                        else:
                            docString = documentationInfo["docstring"]
                    
                    fullText = "".join([header, docString])
                
                self.__plainTextViewer.setText(fullText)
    
    def __showDisabledMessage(self):
        """
        Private method to show a message giving the reason for being disabled.
        """
        if len(self.__providers) == 0:
            self.documentationReady(
                self.tr("No source code documentation provider has been"
                        " registered. This function has been disabled."),
                isWarning=True)
        else:
            self.documentationReady(
                self.tr("This function has been disabled."),
                isWarning=True)
    
    def __setHtml(self, html):
        """
        Private slot to set the prepared HTML text.
        
        @param html prepared HTML text
        @type str
        """
        if self.__richTextViewer:
            self.__richTextViewer.setHtml(html)
    
    def __setHtmlWarning(self, warningText):
        """
        Private slot to set a display message.
        
        @param warningText text to be shown as a warning
        @type str
        """
        if self.__richTextViewer:
            html = prepareDocumentationViewerHtmlWarningDocument(warningText)
            self.__richTextViewer.setHtml(html)
    
    @pyqtSlot(int)
    def on_providerComboBox_currentIndexChanged(self, index):
        """
        Private slot to handle the selection of a provider.
        
        @param index index of the selected provider
        @type int
        """
        if not self.__shuttingDown and not self.__startingUp:
            self.__plainTextViewer.clear()
            if self.__richTextViewer:
                self.__richTextViewer.clear()
            self.objectLineEdit.clear()
            
            provider = self.providerComboBox.itemData(index)
            if provider == self.__disabledProvider:
                self.__showDisabledMessage()
            else:
                self.__lastDocumentation = None
            
            Preferences.setDocuViewer("Provider", provider)
            self.__selectedProvider = provider
    
    def shutdown(self):
        """
        Public method to perform shutdown actions.
        """
        self.__shuttingDown = True
        Preferences.setDocuViewer("Provider", self.__selectedProvider)
    
    def preferencesChanged(self):
        """
        Public slot to handle a change of preferences.
        """
        showMarkdown = Preferences.getDocuViewer("ShowInfoAsRichText")
        if showMarkdown != self.__showMarkdown:
            self.__showTextViewer(showMarkdown)
        
        provider = Preferences.getDocuViewer("Provider")
        self.__selectProvider(provider)
    
    def __selectProvider(self, provider):
        """
        Private method to select a provider programmatically.
        
        @param provider name of the provider to be selected
        @type str
        """
        if provider != self.__selectedProvider:
            index = self.providerComboBox.findData(provider)
            if index < 0:
                index = 0
            self.providerComboBox.setCurrentIndex(index)
    
    def __showTextViewer(self, richText):
        """
        Private slot to show the selected viewer.
        
        @param richText flag indicating the rich text viewer
        @type bool
        """
        self.__showMarkdown = richText
        
        self.__plainTextViewer.clear()
        self.__plainTextViewer.setVisible(not richText)
        
        if self.__richTextViewer:
            self.__richTextViewer.clear()
            self.__richTextViewer.setVisible(richText)
        
        self.__plainTextAct.setChecked(not richText)
        if self.__richTextAct:
            self.__richTextAct.setChecked(richText)
        
        self.documentationReady(self.__lastDocumentation)
        
        Preferences.setDocuViewer("ShowInfoAsRichText", richText)
