# Copyright (c) 2020 gevent contributors.
"""
This module provides :class:`GeventSelector`, a high-level IO
multiplexing mechanism. This is aliased to :class:`DefaultSelector`.

This module provides the same API as the selectors defined in :mod:`selectors`.

On Python 2, this module is only available if the `selectors2
<https://pypi.org/project/selectors2/>`_ backport is installed.

.. versionadded:: 20.6.0
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from collections import defaultdict

try:
    import selectors as __selectors__
except ImportError:
    # Probably on Python 2. Do we have the backport?
    import selectors2 as __selectors__
    __target__ = 'selectors2'

from gevent.hub import _get_hub_noargs as get_hub
from gevent import sleep
from gevent._compat import iteritems
from gevent._compat import itervalues
from gevent._util import copy_globals
from gevent._util import Lazy

from gevent.event import Event
from gevent.select import _EV_READ
from gevent.select import _EV_WRITE

__implements__ = [
    'DefaultSelector',
]
__extra__ = [
    'GeventSelector',
]
__all__ = __implements__ + __extra__

__imports__ = copy_globals(
    __selectors__, globals(),
    names_to_ignore=__all__,
    # Copy __all__; __all__ is defined by selectors2 but not Python 3.
    dunder_names_to_keep=('__all__',)
)

_POLL_ALL = _EV_READ | _EV_WRITE

EVENT_READ = __selectors__.EVENT_READ
EVENT_WRITE = __selectors__.EVENT_WRITE
_ALL_EVENTS = EVENT_READ | EVENT_WRITE
SelectorKey = __selectors__.SelectorKey

# In 3.4 and selectors2, BaseSelector is a concrete
# class that can be called. In 3.5 and later, it's an
# ABC, with the real implementation being
# passed to _BaseSelectorImpl.
_BaseSelectorImpl = getattr(
    __selectors__,
    '_BaseSelectorImpl',
    __selectors__.BaseSelector
)

class GeventSelector(_BaseSelectorImpl):
    """
    A selector implementation using gevent primitives.

    This is a type of :class:`selectors.BaseSelector`, so the documentation
    for that class applies here.

    .. caution::
       As the base class indicates, it is critically important to
       unregister file objects before closing them. (Or close the selector
       they are registered with before closing them.) Failure to do so
       may crash the process or have other unintended results.
    """

    # Notes on the approach:
    #
    # It's easy to wrap a selector implementation around
    # ``gevent.select.poll``; in fact that's what happens by default
    # when monkey-patching in Python 3. But the problem with that is
    # each call to ``selector.select()`` will result in creating and
    # then destroying new kernel-level polling resources, as nothing
    # in ``gevent.select`` can keep watchers around (because the underlying
    # file could be closed at any time). This ends up producing a large
    # number of syscalls that are unnecessary.
    #
    # So here, we take advantage of the fact that it is documented and
    # required that files not be closed while they are registered.
    # This lets us persist watchers. Indeed, it lets us continually
    # accrue events in the background before a call to ``select()`` is even
    # made. We can take advantage of this to return results immediately, without
    # a syscall, if we have them.
    #
    # We create watchers in ``register()`` and destroy them in
    # ``unregister()``. They do not get started until the first call
    # to ``select()``, though. Once they are started, they don't get
    # stopped until they deliver an event.
    # Lifecycle:
    # register() -> inactive_watchers
    # select() -> inactive_watchers -> active_watchers;
    #             active_watchers   -> inactive_watchers

    def __init__(self, hub=None):
        if hub is not None:
            self.hub = hub
        # {fd: watcher}
        self._active_watchers = {}
        self._inactive_watchers = {}
        # {fd: EVENT_READ|EVENT_WRITE}
        self._accumulated_events = defaultdict(int)
        self._ready = Event()
        super(GeventSelector, self).__init__()

    if not hasattr(_BaseSelectorImpl, '_key_from_fd'):
        def _key_from_fd(self, fd):
            # Removed in 3.13; this duplicates the old function.
            try:
                return self._fd_to_key[fd]
            except KeyError:
                return None


    def __callback(self, events, fd):
        if events > 0:
            cur_event_for_fd = self._accumulated_events[fd]
            if events & _EV_READ:
                cur_event_for_fd |= EVENT_READ
            if events & _EV_WRITE:
                cur_event_for_fd |= EVENT_WRITE
            self._accumulated_events[fd] = cur_event_for_fd

        self._ready.set()

    @Lazy
    def hub(self): # pylint:disable=method-hidden
        return get_hub()

    def register(self, fileobj, events, data=None):
        """
        Register a file object for selection, monitoring it for I/O events.

        *fileobj* is the file object to monitor. It may either be an integer file descriptor
        or an object with a ``fileno()`` method. *events* is a bitwise mask of events to
        monitor. *data* is an opaque object.

        :return: A new `SelectorKey` instance.
        :raises ValueError: In case of invalid
            event mask or file descriptor
        :raises KeyError: if the file object is already registered.

        .. versionchanged:: 25.8.1
           More reliably raises a ``ValueError`` if the file descriptor
           is invalid.
        """
        # Handles checking *events* to be valid, and raising the KeyError.
        key = _BaseSelectorImpl.register(self, fileobj, events, data)

        if events == _ALL_EVENTS:
            flags = _POLL_ALL
        elif events == EVENT_READ:
            flags = _EV_READ
        else:
            flags = _EV_WRITE


        loop = self.hub.loop
        io = loop.io
        MAXPRI = loop.MAXPRI

        try:
            self._inactive_watchers[key.fd] = watcher = io(key.fd, flags)
        except OSError as e:
            raise ValueError('Invalid file descriptor') from e
        watcher.priority = MAXPRI
        return key

    def unregister(self, fileobj):
        key = _BaseSelectorImpl.unregister(self, fileobj)
        if key.fd in self._active_watchers:
            watcher = self._active_watchers.pop(key.fd)
        else:
            watcher = self._inactive_watchers.pop(key.fd)
        watcher.stop()
        watcher.close()
        self._accumulated_events.pop(key.fd, None)
        return key

    # XXX: Can we implement ``modify`` more efficiently than
    # ``unregister()``+``register()``? We could detect the no-change
    # case and do nothing; recent versions of the standard library
    # do that.

    def select(self, timeout=None):
        """
        Poll for I/O.

        Note that, like the built-in selectors, this will block
        indefinitely if no timeout is given and no files have been
        registered.
        """
        # timeout > 0 : block seconds
        # timeout <= 0 : No blocking.
        # timeout = None: Block forever

        # Event.wait doesn't deal with negative values
        if timeout is not None and timeout < 0:
            timeout = 0

        # Start any watchers that need started. Note that they may
        # not actually get a chance to do anything yet if we already had
        # events set.
        for fd, watcher in iteritems(self._inactive_watchers):
            watcher.start(self.__callback, fd, pass_events=True)
        self._active_watchers.update(self._inactive_watchers)
        self._inactive_watchers.clear()

        # The _ready event is either already set (in which case
        # there are some results waiting in _accumulated_events) or
        # not set, in which case we have to block. But to make the two cases
        # behave the same, we will always yield to the event loop.
        if self._ready.is_set():
            sleep()
        self._ready.wait(timeout)
        self._ready.clear()
        # TODO: If we have nothing ready, but they ask us not to block,
        # should we make an effort to actually spin the event loop and let
        # it check for events?

        result = []
        for fd, event in iteritems(self._accumulated_events):
            key = self._key_from_fd(fd)
            watcher = self._active_watchers.pop(fd)

            ## The below is taken without comment from
            ## https://github.com/gevent/gevent/pull/1523/files and
            ## hasn't been checked:
            #
            # Since we are emulating an epoll object within another epoll object,
            # once a watcher has fired, we must deactivate it until poll is called
            # next. If we did not, someone else could call, e.g., gevent.time.sleep
            # and any unconsumed bytes on our watched fd would prevent the process
            # from sleeping correctly.
            watcher.stop()
            if key:
                result.append((key, event & key.events))
                self._inactive_watchers[fd] = watcher
            else: # pragma: no cover
                # If the key was gone, then somehow we've been unregistered.
                # Don't put it back in inactive, close it.
                watcher.close()

        self._accumulated_events.clear()
        return result

    def close(self):
        for d in self._active_watchers, self._inactive_watchers:
            if d is None:
                continue # already closed
            for watcher in itervalues(d):
                watcher.stop()
                watcher.close()
        self._active_watchers = self._inactive_watchers = None
        self._accumulated_events = None
        self.hub = None
        _BaseSelectorImpl.close(self)


DefaultSelector = GeventSelector

def _gevent_do_monkey_patch(patch_request):
    aggressive = patch_request.patch_kwargs['aggressive']
    target_mod = patch_request.target_module

    patch_request.default_patch_items()

    import sys
    if 'selectors' not in sys.modules:
        # Py2: Make 'import selectors' work
        sys.modules['selectors'] = sys.modules[__name__]

    # Python 3 wants to use `select.select` as a member function,
    # leading to this error in selectors.py (because
    # gevent.select.select is not a builtin and doesn't get the
    # magic auto-static that they do):
    #
    #    r, w, _ = self._select(self._readers, self._writers, [], timeout)
    #    TypeError: select() takes from 3 to 4 positional arguments but 5 were given
    #
    # Note that this obviously only happens if selectors was
    # imported after we had patched select; but there is a code
    # path that leads to it being imported first (but now we've
    # patched select---so we can't compare them identically). It also doesn't
    # happen on Windows, because they define a normal method for _select, to work around
    # some weirdness in the handling of the third argument.
    #
    # The backport doesn't have that.
    orig_select_select = patch_request.get_original('select', 'select')
    assert target_mod.select is not orig_select_select
    selectors = __selectors__
    SelectSelector = selectors.SelectSelector
    if hasattr(SelectSelector, '_select') and SelectSelector._select in (
            target_mod.select, orig_select_select
    ):
        from gevent.select import select
        def _select(self, *args, **kwargs): # pylint:disable=unused-argument
            return select(*args, **kwargs)
        selectors.SelectSelector._select = _select
        _select._gevent_monkey = True # prove for test cases

    if aggressive:
        # If `selectors` had already been imported before we removed
        # select.epoll|kqueue|devpoll, these may have been defined in terms
        # of those functions. They'll fail at runtime.
        patch_request.remove_item(
            selectors,
            'EpollSelector',
            'KqueueSelector',
            'DevpollSelector',
        )
        selectors.DefaultSelector = DefaultSelector

    # Python 3.7 refactors the poll-like selectors to use a common
    # base class and capture a reference to select.poll, etc, at
    # import time. selectors tends to get imported early
    # (importing 'platform' does it: platform -> subprocess -> selectors),
    # so we need to clean that up.
    if hasattr(selectors, 'PollSelector') and hasattr(selectors.PollSelector, '_selector_cls'):
        from gevent.select import poll
        selectors.PollSelector._selector_cls = poll
