# coding=utf-8
# pynput
# Copyright (C) 2015-2024 Moses Palmér
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This program 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
This module contains the base implementation.

The actual interface to mouse classes is defined here, but the implementation
is located in a platform dependent module.
"""

# pylint: disable=R0903
# We implement stubs

import enum

from pynput._util import AbstractListener, prefix
from pynput import _logger


class Button(enum.Enum):
    """The various buttons.

    The actual values for these items differ between platforms. Some
    platforms may have additional buttons, but these are guaranteed to be
    present everywhere.
    """
    #: An unknown button was pressed
    unknown = 0

    #: The left button
    left = 1

    #: The middle button
    middle = 2

    #: The right button
    right = 3


class Controller(object):
    """A controller for sending virtual mouse events to the system.
    """
    def __init__(self):
        self._log = _logger(self.__class__)

    @property
    def position(self):
        """The current position of the mouse pointer.

        This is the tuple ``(x, y)``, and setting it will move the pointer.
        """
        return self._position_get()

    @position.setter
    def position(self, pos):
        self._position_set(pos)

    def scroll(self, dx, dy):
        """Sends scroll events.

        :param int dx: The horizontal scroll. The units of scrolling is
            undefined.

        :param int dy: The vertical scroll. The units of scrolling is
            undefined.

        :raises ValueError: if the values are invalid, for example out of
            bounds
        """
        self._scroll(dx, dy)

    def press(self, button):
        """Emits a button press event at the current position.

        :param Button button: The button to press.
        """
        self._press(button)

    def release(self, button):
        """Emits a button release event at the current position.

        :param Button button: The button to release.
        """
        self._release(button)

    def move(self, dx, dy):
        """Moves the mouse pointer a number of pixels from its current
        position.

        :param int dx: The horizontal offset.

        :param int dy: The vertical offset.

        :raises ValueError: if the values are invalid, for example out of
            bounds
        """
        self.position = tuple(sum(i) for i in zip(self.position, (dx, dy)))

    def click(self, button, count=1):
        """Emits a button click event at the current position.

        The default implementation sends a series of press and release events.

        :param Button button: The button to click.

        :param int count: The number of clicks to send.
        """
        with self as controller:
            for _ in range(count):
                controller.press(button)
                controller.release(button)

    def __enter__(self):
        """Begins a series of clicks.

        In the default :meth:`click` implementation, the return value of this
        method is used for the calls to :meth:`press` and :meth:`release`
        instead of ``self``.

        The default implementation is a no-op.
        """
        return self

    def __exit__(self, exc_type, value, traceback):
        """Ends a series of clicks.
        """
        pass

    def _position_get(self):
        """The implementation of the getter for :attr:`position`.

        This is a platform dependent implementation.
        """
        raise NotImplementedError()

    def _position_set(self, pos):
        """The implementation of the setter for :attr:`position`.

        This is a platform dependent implementation.
        """
        raise NotImplementedError()

    def _scroll(self, dx, dy):
        """The implementation of the :meth:`scroll` method.

        This is a platform dependent implementation.
        """
        raise NotImplementedError()

    def _press(self, button):
        """The implementation of the :meth:`press` method.

        This is a platform dependent implementation.
        """
        raise NotImplementedError()

    def _release(self, button):
        """The implementation of the :meth:`release` method.

        This is a platform dependent implementation.
        """
        raise NotImplementedError()


# pylint: disable=W0223; This is also an abstract class
class Listener(AbstractListener):
    """A listener for mouse events.

    Instances of this class can be used as context managers. This is equivalent
    to the following code::

        listener.start()
        try:
            listener.wait()
            with_statements()
        finally:
            listener.stop()

    This class inherits from :class:`threading.Thread` and supports all its
    methods. It will set :attr:`daemon` to ``True`` when created.

    :param callable on_move: The callback to call when mouse move events occur.

        It will be called with the arguments ``(x, y)``, which is the new
        pointer position. If this callback raises :class:`StopException` or
        returns ``False``, the listener is stopped.

    :param callable on_click: The callback to call when a mouse button is
        clicked.

        It will be called with the arguments ``(x, y, button, pressed)``,
        where ``(x, y)`` is the new pointer position, ``button`` is one of the
        :class:`Button` values and ``pressed`` is whether the button was
        pressed.

        If this callback raises :class:`StopException` or returns ``False``,
        the listener is stopped.

    :param callable on_scroll: The callback to call when mouse scroll
        events occur.

        It will be called with the arguments ``(x, y, dx, dy)``, where
        ``(x, y)`` is the new pointer position, and ``(dx, dy)`` is the scroll
        vector.

        If this callback raises :class:`StopException` or returns ``False``,
        the listener is stopped.

    :param bool suppress: Whether to suppress events. Setting this to ``True``
        will prevent the input events from being passed to the rest of the
        system.

    :param kwargs: Any non-standard platform dependent options. These should be
        prefixed with the platform name thus: ``darwin_``, ``xorg_`` or
        ``win32_``.

        Supported values are:

        ``darwin_intercept``
            A callable taking the arguments ``(event_type, event)``, where
            ``event_type`` is any mouse related event type constant, and
            ``event`` is a ``CGEventRef``.

            This callable can freely modify the event using functions like
            ``Quartz.CGEventSetIntegerValueField``. If this callable does not
            return the event, the event is suppressed system wide.

        ``win32_event_filter``
            A callable taking the arguments ``(msg, data)``, where ``msg`` is
            the current message, and ``data`` associated data as a
            `MSLLHOOKSTRUCT <https://docs.microsoft.com/en-gb/windows/win32/api/winuser/ns-winuser-msllhookstruct>`_.

            If this callback returns ``False``, the event will not
            be propagated to the listener callback.

            If ``self.suppress_event()`` is called, the event is suppressed
            system wide.
    """
    def __init__(self, on_move=None, on_click=None, on_scroll=None,
                 suppress=False, **kwargs):
        self._log = _logger(self.__class__)
        option_prefix = prefix(Listener, self.__class__)
        self._options = {
            key[len(option_prefix):]: value
            for key, value in kwargs.items()
            if key.startswith(option_prefix)}
        super(Listener, self).__init__(
            on_move=on_move, on_click=on_click, on_scroll=on_scroll,
            suppress=suppress)
# pylint: enable=W0223
