# Standard library imports
import re
from textwrap import dedent

# System library imports
from PyQt4 import QtCore, QtGui


class CallTipWidget(QtGui.QLabel):
    """ Shows call tips by parsing the current text of Q[Plain]TextEdit.
    """

    #--------------------------------------------------------------------------
    # 'QObject' interface
    #--------------------------------------------------------------------------

    def __init__(self, parent):
        """ Create a call tip manager that is attached to the specified Qt
            text edit widget.
        """
        assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
        QtGui.QLabel.__init__(self, parent, QtCore.Qt.ToolTip)

        self.setFont(parent.document().defaultFont())
        self.setForegroundRole(QtGui.QPalette.ToolTipText)
        self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
        self.setPalette(QtGui.QToolTip.palette())

        self.setAlignment(QtCore.Qt.AlignLeft)
        self.setIndent(1)
        self.setFrameStyle(QtGui.QFrame.NoFrame)
        self.setMargin(1 + self.style().pixelMetric(
                QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self))
        self.setWindowOpacity(self.style().styleHint(
                QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self) / 255.0)

    #--------------------------------------------------------------------------
    # 'QWidget' interface
    #--------------------------------------------------------------------------

    def hideEvent(self, event):
        """ Reimplemented to disconnect the cursor movement handler.
        """
        QtGui.QLabel.hideEvent(self, event)
        self.parent().cursorPositionChanged.disconnect(self._update_tip)

    def keyPressEvent(self, event):
        """ Reimplemented to hide on certain key presses.
        """
        if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return, 
                           QtCore.Qt.Key_Escape):
            self.hide()

    def paintEvent(self, event):
        """ Reimplemented to paint the background panel.
        """
        painter = QtGui.QStylePainter(self)
        option = QtGui.QStyleOptionFrame()
        option.init(self)
        painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option)
        painter.end()

        QtGui.QLabel.paintEvent(self, event)

    def showEvent(self, event):
        """ Reimplemented to connect the cursor movement handler.
        """
        QtGui.QLabel.showEvent(self, event)
        self.parent().cursorPositionChanged.connect(self._update_tip)

    #--------------------------------------------------------------------------
    # 'CallTipWidget' interface
    #--------------------------------------------------------------------------

    def show_docstring(self, doc, maxlines=20):
        """ Attempts to show the specified docstring at the current cursor
            location. The docstring is dedented and possibly truncated for
            length.
        """
        doc = dedent(doc.rstrip()).lstrip()
        match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc)
        if match:
            doc = doc[:match.end()] + '\n[Documentation continues...]'
        return self.show_tip(doc)

    def show_tip(self, tip):
        """ Attempts to show the specified tip at the current cursor location.
        """
        text_edit = self.parent()
        document = text_edit.document()
        cursor = text_edit.textCursor()
        search_pos = cursor.position() - 1
        self._start_position, _ = self._find_parenthesis(search_pos, 
                                                         forward=False)
        if self._start_position == -1:
            return False
    
        point = text_edit.cursorRect(cursor).bottomRight()
        point = text_edit.mapToGlobal(point)
        self.move(point)
        self.setText(tip)
        if self.isVisible():
            self.resize(self.sizeHint())
        else:
            self.show()
        return True
            
    #--------------------------------------------------------------------------
    # Protected interface
    #--------------------------------------------------------------------------

    def _find_parenthesis(self, position, forward=True):
        """ If 'forward' is True (resp. False), proceed forwards
            (resp. backwards) through the line that contains 'position' until an
            unmatched closing (resp. opening) parenthesis is found. Returns a
            tuple containing the position of this parenthesis (or -1 if it is
            not found) and the number commas (at depth 0) found along the way.
        """
        commas = depth = 0
        document = self.parent().document()
        qchar = document.characterAt(position)
        while (position > 0 and qchar.isPrint() and 
               # Need to check explicitly for line/paragraph separators:
               qchar.unicode() not in (0x2028, 0x2029)):
            char = qchar.toAscii()
            if char == ',' and depth == 0:
                commas += 1
            elif char == ')':
                if forward and depth == 0:
                    break
                depth += 1
            elif char == '(':
                if not forward and depth == 0:
                    break
                depth -= 1
            position += 1 if forward else -1
            qchar = document.characterAt(position)
        else:
            position = -1
        return position, commas

    def _highlight_tip(self, tip, current_argument):
        """ Highlight the current argument (arguments start at 0), ending at the
            next comma or unmatched closing parenthesis.
            
            FIXME: This is an unreliable way to do things and it isn't being
                   used right now. Instead, we should use inspect.getargspec
                   metadata for this purpose.
        """
        start = tip.find('(')
        if start != -1:
            for i in xrange(current_argument):
                start = tip.find(',', start)
            if start != -1:
                end = start + 1
                while end < len(tip):
                    char = tip[end]
                    depth = 0
                    if (char == ',' and depth == 0):
                        break
                    elif char == '(':
                        depth += 1
                    elif char == ')':
                        if depth == 0:
                            break
                        depth -= 1
                    end += 1
                tip = tip[:start+1] + '<font color="blue">' + \
                    tip[start+1:end] + '</font>' + tip[end:]
                tip = tip.replace('\n', '<br/>')
        return tip

    def _update_tip(self):
        """ Updates the tip based on user cursor movement.
        """
        cursor = self.parent().textCursor()
        if cursor.position() <= self._start_position:
            self.hide()
        else:
            position, commas = self._find_parenthesis(self._start_position + 1)
            if position != -1:
                self.hide()