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

# Copyright 2016-2018 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/>.

"""Mouse handling for a browser tab."""

from PyQt5.QtCore import QObject, QEvent, Qt, QTimer

from qutebrowser.config import config
from qutebrowser.utils import message, log, usertypes, qtutils, objreg
from qutebrowser.keyinput import modeman


class ChildEventFilter(QObject):

    """An event filter re-adding MouseEventFilter on ChildEvent.

    This is needed because QtWebEngine likes to randomly change its
    focusProxy...

    FIXME:qtwebengine Add a test for this happening

    Attributes:
        _filter: The event filter to install.
        _widget: The widget expected to send out childEvents.
    """

    def __init__(self, eventfilter, widget, win_id, parent=None):
        super().__init__(parent)
        self._filter = eventfilter
        assert widget is not None
        self._widget = widget
        self._win_id = win_id

    def eventFilter(self, obj, event):
        """Act on ChildAdded events."""
        if event.type() == QEvent.ChildAdded:
            child = event.child()
            log.mouse.debug("{} got new child {}, installing filter".format(
                obj, child))
            assert obj is self._widget
            child.installEventFilter(self._filter)

            if qtutils.version_check('5.11', compiled=False, exact=True):
                # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076
                pass_modes = [usertypes.KeyMode.command,
                              usertypes.KeyMode.prompt,
                              usertypes.KeyMode.yesno]
                if modeman.instance(self._win_id).mode not in pass_modes:
                    tabbed_browser = objreg.get('tabbed-browser',
                                                scope='window',
                                                window=self._win_id)
                    current_index = tabbed_browser.widget.currentIndex()
                    try:
                        widget_index = tabbed_browser.widget.indexOf(
                            self._widget.parent())
                    except RuntimeError:
                        widget_index = -1
                    if current_index == widget_index:
                        QTimer.singleShot(0, self._widget.setFocus)

        elif event.type() == QEvent.ChildRemoved:
            child = event.child()
            log.mouse.debug("{}: removed child {}".format(obj, child))

        return False


class MouseEventFilter(QObject):

    """Handle mouse events on a tab.

    Attributes:
        _tab: The browsertab object this filter is installed on.
        _handlers: A dict of handler functions for the handled events.
        _ignore_wheel_event: Whether to ignore the next wheelEvent.
        _check_insertmode_on_release: Whether an insertmode check should be
                                      done when the mouse is released.
    """

    def __init__(self, tab, *, parent=None):
        super().__init__(parent)
        self._tab = tab
        self._handlers = {
            QEvent.MouseButtonPress: self._handle_mouse_press,
            QEvent.MouseButtonRelease: self._handle_mouse_release,
            QEvent.Wheel: self._handle_wheel,
            QEvent.ContextMenu: self._handle_context_menu,
        }
        self._ignore_wheel_event = False
        self._check_insertmode_on_release = False

    def _handle_mouse_press(self, e):
        """Handle pressing of a mouse button."""
        is_rocker_gesture = (config.val.input.rocker_gestures and
                             e.buttons() == Qt.LeftButton | Qt.RightButton)

        if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture:
            self._mousepress_backforward(e)
            return True

        self._ignore_wheel_event = True

        pos = e.pos()
        if pos.x() < 0 or pos.y() < 0:
            log.mouse.warning("Ignoring invalid click at {}".format(pos))
            return False

        if e.button() != Qt.NoButton:
            self._tab.elements.find_at_pos(pos, self._mousepress_insertmode_cb)

        return False

    def _handle_mouse_release(self, _e):
        """Handle releasing of a mouse button."""
        # We want to make sure we check the focus element after the WebView is
        # updated completely.
        QTimer.singleShot(0, self._mouserelease_insertmode)
        return False

    def _handle_wheel(self, e):
        """Zoom on Ctrl-Mousewheel.

        Args:
            e: The QWheelEvent.
        """
        if self._ignore_wheel_event:
            # See https://github.com/qutebrowser/qutebrowser/issues/395
            self._ignore_wheel_event = False
            return True

        if e.modifiers() & Qt.ControlModifier:
            mode = modeman.instance(self._tab.win_id).mode
            if mode == usertypes.KeyMode.passthrough:
                return False

            divider = config.val.zoom.mouse_divider
            if divider == 0:
                return False
            factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider)
            if factor < 0:
                return False
            perc = int(100 * factor)
            message.info("Zoom level: {}%".format(perc), replace=True)
            self._tab.zoom.set_factor(factor)
        elif e.modifiers() & Qt.ShiftModifier:
            if e.angleDelta().y() > 0:
                self._tab.scroller.left()
            else:
                self._tab.scroller.right()
            return True

        return False

    def _handle_context_menu(self, _e):
        """Suppress context menus if rocker gestures are turned on."""
        return config.val.input.rocker_gestures

    def _mousepress_insertmode_cb(self, elem):
        """Check if the clicked element is editable."""
        if elem is None:
            # Something didn't work out, let's find the focus element after
            # a mouse release.
            log.mouse.debug("Got None element, scheduling check on "
                            "mouse release")
            self._check_insertmode_on_release = True
            return

        if elem.is_editable():
            log.mouse.debug("Clicked editable element!")
            if config.val.input.insert_mode.auto_enter:
                modeman.enter(self._tab.win_id, usertypes.KeyMode.insert,
                              'click', only_if_normal=True)
        else:
            log.mouse.debug("Clicked non-editable element!")
            if config.val.input.insert_mode.auto_leave:
                modeman.leave(self._tab.win_id, usertypes.KeyMode.insert,
                              'click', maybe=True)

    def _mouserelease_insertmode(self):
        """If we have an insertmode check scheduled, handle it."""
        if not self._check_insertmode_on_release:
            return
        self._check_insertmode_on_release = False

        def mouserelease_insertmode_cb(elem):
            """Callback which gets called from JS."""
            if elem is None:
                log.mouse.debug("Element vanished!")
                return

            if elem.is_editable():
                log.mouse.debug("Clicked editable element (delayed)!")
                modeman.enter(self._tab.win_id, usertypes.KeyMode.insert,
                              'click-delayed', only_if_normal=True)
            else:
                log.mouse.debug("Clicked non-editable element (delayed)!")
                if config.val.input.insert_mode.auto_leave:
                    modeman.leave(self._tab.win_id, usertypes.KeyMode.insert,
                                  'click-delayed', maybe=True)

        self._tab.elements.find_focused(mouserelease_insertmode_cb)

    def _mousepress_backforward(self, e):
        """Handle back/forward mouse button presses.

        Args:
            e: The QMouseEvent.
        """
        if e.button() in [Qt.XButton1, Qt.LeftButton]:
            # Back button on mice which have it, or rocker gesture
            if self._tab.history.can_go_back():
                self._tab.history.back()
            else:
                message.error("At beginning of history.")
        elif e.button() in [Qt.XButton2, Qt.RightButton]:
            # Forward button on mice which have it, or rocker gesture
            if self._tab.history.can_go_forward():
                self._tab.history.forward()
            else:
                message.error("At end of history.")

    def eventFilter(self, obj, event):
        """Filter events going to a QWeb(Engine)View."""
        evtype = event.type()
        if evtype not in self._handlers:
            return False
        if obj is not self._tab.event_target():
            log.mouse.debug("Ignoring {} to {}".format(
                event.__class__.__name__, obj))
            return False
        return self._handlers[evtype](event)
