# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:

# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser.  If not, see <http://www.gnu.org/licenses/>.

"""Completer attached to a CompletionView."""

import attr
from PyQt5.QtCore import pyqtSlot, QObject, QTimer

from qutebrowser.config import config
from qutebrowser.commands import cmdutils, runners
from qutebrowser.utils import log, utils, debug
from qutebrowser.completion.models import miscmodels


@attr.s
class CompletionInfo:

    """Context passed into all completion functions."""

    config = attr.ib()
    keyconf = attr.ib()


class Completer(QObject):

    """Completer which manages completions in a CompletionView.

    Attributes:
        _cmd: The statusbar Command object this completer belongs to.
        _ignore_change: Whether to ignore the next completion update.
        _timer: The timer used to trigger the completion update.
        _last_cursor_pos: The old cursor position so we avoid double completion
                          updates.
        _last_text: The old command text so we avoid double completion updates.
        _last_completion_func: The completion function used for the last text.
    """

    def __init__(self, cmd, parent=None):
        super().__init__(parent)
        self._cmd = cmd
        self._ignore_change = False
        self._timer = QTimer()
        self._timer.setSingleShot(True)
        self._timer.setInterval(0)
        self._timer.timeout.connect(self._update_completion)
        self._last_cursor_pos = None
        self._last_text = None
        self._last_completion_func = None
        self._cmd.update_completion.connect(self.schedule_completion_update)

    def __repr__(self):
        return utils.get_repr(self)

    def _model(self):
        """Convenience method to get the current completion model."""
        completion = self.parent()
        return completion.model()

    def _get_new_completion(self, before_cursor, under_cursor):
        """Get the completion function based on the current command text.

        Args:
            before_cursor: The command chunks before the cursor.
            under_cursor: The command chunk under the cursor.

        Return:
            A completion model.
        """
        if '--' in before_cursor or under_cursor.startswith('-'):
            # cursor on a flag or after an explicit split (--)
            return None
        log.completion.debug("Before removing flags: {}".format(before_cursor))
        before_cursor = [x for x in before_cursor if not x.startswith('-')]
        log.completion.debug("After removing flags: {}".format(before_cursor))
        if not before_cursor:
            # '|' or 'set|'
            log.completion.debug('Starting command completion')
            return miscmodels.command
        try:
            cmd = cmdutils.cmd_dict[before_cursor[0]]
        except KeyError:
            log.completion.debug("No completion for unknown command: {}"
                                 .format(before_cursor[0]))
            return None
        argpos = len(before_cursor) - 1
        try:
            func = cmd.get_pos_arg_info(argpos).completion
        except IndexError:
            log.completion.debug("No completion in position {}".format(argpos))
            return None
        return func

    def _quote(self, s):
        """Quote s if it needs quoting for the commandline.

        Note we don't use shlex.quote because that quotes a lot of shell
        metachars we don't need to have quoted.
        """
        if not s:
            return "''"
        elif any(c in s for c in ' "\'\t\n\\'):
            # use single quotes, and put single quotes into double quotes
            # the string $'b is then quoted as '$'"'"'b'
            return "'" + s.replace("'", "'\"'\"'") + "'"
        else:
            return s

    def _partition(self):
        """Divide the commandline text into chunks around the cursor position.

        Return:
            ([parts_before_cursor], 'part_under_cursor', [parts_after_cursor])
        """
        text = self._cmd.text()[len(self._cmd.prefix()):]
        if not text or not text.strip():
            # Only ":", empty part under the cursor with nothing before/after
            return [], '', []
        parser = runners.CommandParser()
        result = parser.parse(text, fallback=True, keep=True)
        # pylint: disable=not-an-iterable
        parts = [x for x in result.cmdline if x]
        # pylint: enable=not-an-iterable
        pos = self._cmd.cursorPosition() - len(self._cmd.prefix())
        pos = min(pos, len(text))  # Qt treats 2-byte UTF-16 chars as 2 chars
        log.completion.debug('partitioning {} around position {}'.format(parts,
                                                                         pos))
        for i, part in enumerate(parts):
            pos -= len(part)
            if pos <= 0:
                if part[pos-1:pos+1].isspace():
                    # cursor is in a space between two existing words
                    parts.insert(i, '')
                prefix = [x.strip() for x in parts[:i]]
                center = parts[i].strip()
                # strip trailing whitepsace included as a separate token
                postfix = [x.strip() for x in parts[i+1:] if not x.isspace()]
                log.completion.debug(
                    "partitioned: {} '{}' {}".format(prefix, center, postfix))
                return prefix, center, postfix

        # We should always return above
        assert False, parts

    @pyqtSlot(str)
    def on_selection_changed(self, text):
        """Change the completed part if a new item was selected.

        Called from the views selectionChanged method.

        Args:
            text: Newly selected text.
        """
        if text is None:
            return
        before, center, after = self._partition()
        log.completion.debug("Changing {} to '{}'".format(center, text))
        try:
            maxsplit = cmdutils.cmd_dict[before[0]].maxsplit
        except (KeyError, IndexError):
            maxsplit = None
        if maxsplit is None:
            text = self._quote(text)
        model = self._model()
        if model.count() == 1 and config.val.completion.quick:
            # If we only have one item, we want to apply it immediately
            # and go on to the next part.
            self._change_completed_part(text, before, after, immediate=True)
            if maxsplit is not None and maxsplit < len(before):
                # If we are quick-completing the part after maxsplit, don't
                # keep offering completions (see issue #1519)
                self._ignore_change = True
        else:
            log.completion.debug("Will ignore next completion update.")
            self._ignore_change = True
            self._change_completed_part(text, before, after)

    @pyqtSlot()
    def schedule_completion_update(self):
        """Schedule updating/enabling completion.

        For performance reasons we don't want to block here, instead we do this
        in the background.
        """
        if (self._cmd.cursorPosition() == self._last_cursor_pos and
                self._cmd.text() == self._last_text):
            log.completion.debug("Ignoring update because there were no "
                                 "changes.")
        else:
            log.completion.debug("Scheduling completion update.")
            self._timer.start()
        self._last_cursor_pos = self._cmd.cursorPosition()
        self._last_text = self._cmd.text()

    @pyqtSlot()
    def _update_completion(self):
        """Check if completions are available and activate them."""
        if self._ignore_change:
            log.completion.debug("Ignoring completion update because "
                                 "ignore_change is True.")
            self._ignore_change = False
            return

        completion = self.parent()

        if self._cmd.prefix() != ':':
            # This is a search or gibberish, so we don't need to complete
            # anything (yet)
            # FIXME complete searches
            # https://github.com/qutebrowser/qutebrowser/issues/32
            completion.set_model(None)
            self._last_completion_func = None
            return

        before_cursor, pattern, after_cursor = self._partition()

        log.completion.debug("Updating completion: {} {} {}".format(
            before_cursor, pattern, after_cursor))

        pattern = pattern.strip("'\"")
        func = self._get_new_completion(before_cursor, pattern)

        if func is None:
            log.completion.debug('Clearing completion')
            completion.set_model(None)
            self._last_completion_func = None
            return

        if func != self._last_completion_func:
            self._last_completion_func = func
            args = (x for x in before_cursor[1:] if not x.startswith('-'))
            with debug.log_time(log.completion,
                    'Starting {} completion'.format(func.__name__)):
                info = CompletionInfo(config=config.instance,
                                      keyconf=config.key_instance)
                model = func(*args, info=info)
            with debug.log_time(log.completion, 'Set completion model'):
                completion.set_model(model)

        completion.set_pattern(pattern)

    def _change_completed_part(self, newtext, before, after, immediate=False):
        """Change the part we're currently completing in the commandline.

        Args:
            text: The text to set (string) for the token under the cursor.
            before: Commandline tokens before the token under the cursor.
            after: Commandline tokens after the token under the cursor.
            immediate: True if the text should be completed immediately
                       including a trailing space and we shouldn't continue
                       completing the current item.
        """
        text = self._cmd.prefix() + ' '.join(before + [newtext])
        pos = len(text) + (1 if immediate else 0)
        if after:
            text += ' ' + ' '.join(after)
        elif immediate:
            # pad with a space if quick-completing the last entry
            text += ' '
        log.completion.debug("setting text = '{}', pos = {}".format(text, pos))
        self._cmd.setText(text)
        self._cmd.setCursorPosition(pos)
        self._cmd.setFocus()
        self._cmd.show_cmd.emit()
