# Copyright (c) 2009-2014 Denis Bilenko and gevent contributors. See LICENSE for details.

"""Cooperative low-level networking interface.

This module provides socket operations and some related functions.
The API of the functions and classes matches the API of the corresponding
items in the standard :mod:`socket` module exactly, but the synchronous functions
in this module only block the current greenlet and let the others run.

For convenience, exceptions (like :class:`error <socket.error>` and :class:`timeout <socket.timeout>`)
as well as the constants from the :mod:`socket` module are imported into this module.
"""
# Our import magic sadly makes this warning useless
# pylint: disable=undefined-variable

from gevent._compat import PY3
from gevent._compat import PY311
from gevent._compat import exc_clear
from gevent._util import copy_globals


if PY3:
    from gevent import _socket3 as _source # python 2: pylint:disable=no-name-in-module
else:
    from gevent import _socket2 as _source

# define some things we're expecting to overwrite; each module
# needs to define these
__implements__ = __dns__ = __all__ = __extensions__ = __imports__ = ()


class error(Exception):
    errno = None


def getfqdn(*args):
    # pylint:disable=unused-argument
    raise NotImplementedError()

copy_globals(_source, globals(),
             dunder_names_to_keep=('__implements__', '__dns__', '__all__',
                                   '__extensions__', '__imports__', '__socket__'),
             cleanup_globs=False)

# The _socket2 and _socket3 don't import things defined in
# __extensions__, to help avoid confusing reference cycles in the
# documentation and to prevent importing from the wrong place, but we
# *do* need to expose them here. (NOTE: This may lead to some sphinx
# warnings like:
#    WARNING: missing attribute mentioned in :members: or __all__:
#             module gevent._socket2, attribute cancel_wait
# These can be ignored.)
from gevent import _socketcommon
copy_globals(_socketcommon, globals(),
             only_names=_socketcommon.__extensions__)

try:
    _GLOBAL_DEFAULT_TIMEOUT = __socket__._GLOBAL_DEFAULT_TIMEOUT
except AttributeError:
    _GLOBAL_DEFAULT_TIMEOUT = object()


def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None, **kwargs):
    """
    create_connection(address, timeout=None, source_address=None, *, all_errors=False) -> socket

    Connect to *address* and return the :class:`gevent.socket.socket`
    object.

    Convenience function. Connect to *address* (a 2-tuple ``(host,
    port)``) and return the socket object. Passing the optional
    *timeout* parameter will set the timeout on the socket instance
    before attempting to connect. If no *timeout* is supplied, the
    global default timeout setting returned by
    :func:`getdefaulttimeout` is used. If *source_address* is set it
    must be a tuple of (host, port) for the socket to bind as a source
    address before making the connection. A host of '' or port 0 tells
    the OS to use the default.

    .. versionchanged:: 20.6.0
        If the host part of the address includes an IPv6 scope ID,
        it will be used instead of ignored, if the platform supplies
        :func:`socket.inet_pton`.
    .. versionchanged:: 22.08.0
        Add the *all_errors* argument. This only has meaning on Python 3.11;
        it is a programming error to pass it on earlier versions.
    """
    # Sigh. This function is a near-copy of the CPython implementation.
    # Even though we simplified some things, it's still a little complex to
    # cope with error handling, which got even more complicated in 3.11.
    # pylint:disable=too-many-locals,too-many-branches

    all_errors = False
    if PY311:
        all_errors = kwargs.pop('all_errors', False)
    if kwargs:
        raise TypeError("Too many keyword arguments to create_connection", kwargs)


    host, port = address
    exceptions = []
    # getaddrinfo is documented as returning a list, but our interface
    # is pluggable, so be sure it does.
    addrs = list(getaddrinfo(host, port, 0, SOCK_STREAM))
    if not addrs:
        raise error("getaddrinfo returns an empty list")

    for res in addrs:
        af, socktype, proto, _canonname, sa = res
        sock = None
        try:
            sock = socket(af, socktype, proto)
            if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
                sock.settimeout(timeout)
            if source_address:
                sock.bind(source_address)
            sock.connect(sa)

        except error as exc:
            if not all_errors:
                exceptions = [exc] # raise only the last error
            else:
                exceptions.append(exc)
            del exc # cycle
            if sock is not None:
                sock.close()
            sock = None
            if res is addrs[-1]:
                if not all_errors:
                    del exceptions[:]
                    raise
                try:
                    raise ExceptionGroup("create_connection failed", exceptions)
                finally:
                    # Break explicitly a reference cycle
                    del exceptions[:]
            # without exc_clear(), if connect() fails once, the socket
            # is referenced by the frame in exc_info and the next
            # bind() fails (see test__socket.TestCreateConnection)
            # that does not happen with regular sockets though,
            # because _socket.socket.connect() is a built-in. this is
            # similar to "getnameinfo loses a reference" failure in
            # test_socket.py
            exc_clear()
        except BaseException:
            # Things like GreenletExit,  Timeout and KeyboardInterrupt.
            # These get raised immediately, being sure to
            # close the socket
            if sock is not None:
                sock.close()
            sock = None
            raise
        else:
            # break reference cycles
            del exceptions[:]
            try:
                return sock
            finally:
                sock = None


# This is promised to be in the __all__ of the _source, but, for circularity reasons,
# we implement it in this module. Mostly for documentation purposes, put it
# in the _source too.
_source.create_connection = create_connection
