# -*- coding: utf-8 -*-
# Copyright (C) 2013, the codeeditor development team
#
# Pyzo is distributed under the terms of the 2-Clause BSD License.
# The full license can be found in 'license.txt'.

"""
Code editor extensions that provides autocompleter functionality
"""

import pyzo
from ..qt import QtGui, QtCore, QtWidgets

Qt = QtCore.Qt

import keyword

# TODO: use this CompletionListModel to style the completion suggestions (class names, method names, keywords etc)
class CompletionListModel(QtCore.QStringListModel):
    def data(self, index, role):
        if role == Qt.ForegroundRole:
            # data = str(QtWidgets.QStringListModel.data(self, index, QtCore.Qt.DisplayRole))
            # return QtGui.QBrush(Qt.red)
            return None
        else:
            return QtCore.QStringListModel.data(self, index, role)


# todo: use keywords from the parser
class AutoCompletion(object):
    def __init__(self, *args, **kwds):
        super(AutoCompletion, self).__init__(*args, **kwds)
        # Autocompleter
        self.__completerModel = QtCore.QStringListModel(keyword.kwlist)
        self.__completer = QtWidgets.QCompleter(self)
        self.__completer.setModel(self.__completerModel)
        self.__completer.setCaseSensitivity(Qt.CaseInsensitive)
        self.__completer.setWidget(self)
        self.__completerNames = []
        self.__recentCompletions = []  # List of recently selected completions

        self.__cancelCallback = None

        # geometry
        self.__popupSize = 300, 100

        # Text position corresponding to first charcter of the word being completed
        self.__autocompleteStart = None

        # We show the popup when this many chars have been input
        self.__autocompleteMinChars = 3
        self.__autocompleteFromObject = False
        self.__autocompleteVisible = False

        self.__autocompleteDebug = False

        self.__autocompletionAcceptKeys = (Qt.Key_Tab,)

        # Connect signals
        self.__highlightedCompletion = None
        self.__completer.activated.connect(self.onAutoComplete)
        self.__completer.highlighted.connect(self._setHighlightedCompletion)

    def _setHighlightedCompletion(self, value):
        """Keeping track of the highlighted item allows us
        to 'manually' perform an autocompletion.
        """
        self.__highlightedCompletion = value

    ## Properties
    def recentCompletionsList(self):
        """
        The list of recent auto-completions. This property may be set to a
        list that is shared among several editors, in order to share the notion
        of recent auto-completions
        """
        return self.__recentCompletions

    def setRecentCompletionsList(self, value):
        self.__recentCompletions = value

    def completer(self):
        return self.__completer

    def setAutoCompletionAcceptKeys(self, *keys):
        """Set the keys (Qt enums) that can accept an autocompletion.
        Like Tab, or Enter. Defaut Tab.
        """
        self.__autocompletionAcceptKeys = keys

    def setCancelCallback(self, cb):
        self.__cancelCallback = cb

    ## Autocompletion

    def setAutocompletPopupSize(self, width, height):
        """
        Set the size (width, heigth) of the automcompletion popup window.
        """
        self.__popupSize = width, height

    def setAutocompleteMinChars(self, n):
        """
        Set the number of chars where we show the popup.
        """
        self.__autocompleteMinChars = n

    def autocompleteShow(self, offset=0, names=None, fromObject=False):
        """
        Pop-up the autocompleter (if not already visible) and position it at current
        cursor position minus offset. If names is given and not None, it is set
        as the list of possible completions.
        """
        # Pop-up the autocompleteList
        startcursor = self.textCursor()
        startcursor.movePosition(startcursor.Left, n=offset)
        self.__autocompleteFromObject = fromObject

        if self.__autocompleteDebug:
            print("autocompleteShow called")

        if names is not None:
            # TODO: a more intelligent implementation that adds new items and removes
            # old ones
            if names != self.__completerNames:
                self.__completerModel.setStringList(names)
                self.__completerNames = names

        if (
            not self.autocompleteActive()
            or startcursor.position() != self.__autocompleteStart.position()
            or not self.autocompleteVisible()
        ):

            self.__autocompleteStart = startcursor
            self.__autocompleteStart.setKeepPositionOnInsert(True)

            # Popup the autocompleter. Don't use .complete() since we want to
            # position the popup manually
            self.__positionAutocompleter()
            if self.__updateAutocompleterPrefix():
                self.__autocompleteVisible = True
                self.__completer.popup().show()
            if self.__autocompleteDebug:
                print("self.__completer.popup().show() called")

        else:
            self.__updateAutocompleterPrefix()

    def autocompleteAccept(self):
        self.__completer.popup().hide()
        self.__autocompleteStart = None
        self.__autocompleteVisible = False

    def autocompleteCancel(self):
        self.__completer.popup().hide()
        self.__autocompleteStart = None
        self.__autocompleteVisible = False
        if self.__cancelCallback is not None:
            try:
                self.__cancelCallback()
            except Exception as err:
                print("Exception in autocomp cancel callback: " + str(err))

    def onAutoComplete(self, text=None):
        if text is None:
            text = self.__highlightedCompletion
        # Select the text from autocompleteStart until the current cursor
        cursor = self.textCursor()
        cursor.setPosition(self.__autocompleteStart.position(), cursor.KeepAnchor)
        # Replace it with the selected text
        cursor.insertText(text)
        self.autocompleteAccept()  # Reset the completer

        # Update the recent completions list
        if text in self.__recentCompletions:
            self.__recentCompletions.remove(text)
        self.__recentCompletions.append(text)

    def autocompleteActive(self):
        """Returns whether an autocompletion list is currently started."""
        return self.__autocompleteStart is not None

    def autocompleteVisible(self):
        """Returns whether an autocompletion list is currently shown."""
        return self.__autocompleteVisible

    def __positionAutocompleter(self):
        """Move the autocompleter list to a proper position"""
        # Find the start of the autocompletion and move the completer popup there
        cur = QtGui.QTextCursor(self.__autocompleteStart)  # Copy __autocompleteStart

        # Set size
        geometry = self.__completer.popup().geometry()
        geometry.setWidth(self.__popupSize[0])
        geometry.setHeight(self.__popupSize[1])
        self.__completer.popup().setGeometry(geometry)

        # Initial choice for position of the completer
        position = self.cursorRect(cur).bottomLeft() + self.viewport().pos()

        # Check if the completer is going to go off the screen
        # top_geometry = QtWidgets.qApp.qApp.screens()[0].availableVirtualSize()
        top_geometry = pyzo.main.geometry()
        global_position = self.mapToGlobal(position)
        if global_position.y() + geometry.height() > top_geometry.height():
            # Move the completer to above the current line
            position = self.cursorRect(cur).topLeft() + self.viewport().pos()
            global_position = self.mapToGlobal(position)
            global_position -= QtCore.QPoint(0, int(geometry.height()))

        self.__completer.popup().move(global_position)

    def __updateAutocompleterPrefix(self):
        """
        Find the autocompletion prefix (the part of the word that has been
        entered) and send it to the completer. Update the selected completion
        (out of several possiblilties) which is best suited
        """
        if not self.autocompleteActive():
            self.__completer.popup().hide()  # TODO: why is this required?
            self.__autocompleteVisible = False
            return False

        # Select the text from autocompleteStart until the current cursor
        cursor = self.textCursor()
        cursor.setPosition(self.__autocompleteStart.position(), cursor.KeepAnchor)

        prefix = cursor.selectedText()
        if (
            not self.__autocompleteFromObject
            and len(prefix) < self.__autocompleteMinChars
        ):
            self.__completer.setCompletionPrefix("")
            self.autocompleteCancel()
            return False
        else:
            self.__completer.setCompletionPrefix(prefix)
            model = self.__completer.completionModel()
            if model.rowCount():
                # Create a list of all possible completions, and select the one
                # which is best suited. Use the one which is highest in the
                # __recentCompletions list, but prefer completions with matching
                # case if they exists

                # Create a list of (row, value) tuples of all possible completions
                completions = [
                    (
                        row,
                        model.data(
                            model.index(row, 0), self.__completer.completionRole()
                        ),
                    )
                    for row in range(model.rowCount())
                ]

                # Define a function to get the position in the __recentCompletions
                def completionIndex(data):
                    try:
                        return self.__recentCompletions.index(data)
                    except ValueError:
                        return -1

                # Sort twice; the last sort has priority over the first

                # Sort on most recent completions
                completions.sort(key=lambda c: completionIndex(c[1]), reverse=True)
                # Sort on matching case (prefer matching case)
                completions.sort(key=lambda c: c[1].startswith(prefix), reverse=True)

                # apply the best match
                bestMatchRow = completions[0][0]
                self.__completer.popup().setCurrentIndex(model.index(bestMatchRow, 0))

                return True

            else:
                # No match, just hide
                self.autocompleteCancel()
                return False

    def potentiallyAutoComplete(self, event):
        """potentiallyAutoComplete(event)
        Given a keyEvent, check if we should perform an autocompletion.
        Returns 0 if no autocompletion was performed. Return 1 if
        autocompletion was performed, but the key event should be processed
        as normal. Return 2 if the autocompletion was performed, and the key
        should be consumed.
        """
        if self.autocompleteActive():
            if event.key() in self.__autocompletionAcceptKeys:
                if event.key() <= 128:
                    self.onAutoComplete()  # No arg: select last highlighted
                    event.ignore()
                    return 1  # Let key have effect as normal
                elif event.modifiers() == Qt.NoModifier:
                    # The key
                    self.onAutoComplete()  # No arg: select last highlighted
                    return 2  # Key should be consumed
        return 0

    def keyPressEvent(self, event):
        key = event.key()
        modifiers = event.modifiers()
        if (
            key == Qt.Key_Escape
            and modifiers == Qt.NoModifier
            and self.autocompleteActive()
        ):
            self.autocompleteCancel()
            return  # Consume the key

        if self.potentiallyAutoComplete(event) > 1:
            return  # Consume

        # Allowed keys that do not close the autocompleteList:
        # alphanumeric and _ ans shift
        # Backspace (until start of autocomplete word)
        if (
            self.autocompleteActive()
            and not event.text().isalnum()
            and event.text() != "_"
            and key != Qt.Key_Shift
            and not (
                (key == Qt.Key_Backspace)
                and self.textCursor().position() > self.__autocompleteStart.position()
            )
        ):
            self.autocompleteCancel()

        # Apply the key that was pressed
        super(AutoCompletion, self).keyPressEvent(event)

        if self.autocompleteActive():
            # While we type, the start of the autocompletion may move due to line
            # wrapping, so reposition after every key stroke
            self.__positionAutocompleter()
            self.__updateAutocompleterPrefix()
