# Copyright (c) 2016 Ian C. Good
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#

"""Package containing a variety of useful modules utilities that didn't really
belong anywhere else.

"""

from __future__ import absolute_import

from gevent import socket

__all__ = ['build_ipv4_socket_creator', 'create_connection_ipv4',
           'create_listeners']


def build_ipv4_socket_creator(only_ports=None):
    """Returns a function that will act like
    :py:func:`socket.create_connection` but only using IPv4 addresses. This
    function can be used as the ``socket_creator`` argument to some classes
    like :class:`~slimta.relay.smtp.mx.MxSmtpRelay`.

    :param only_ports: If given, can be a list to limit which ports are
                       restricted to IPv4. Connections to all other ports may
                       be IPv6.

    """
    def socket_creator(*args, **kwargs):
        return create_connection_ipv4(*args, only_ports=only_ports, **kwargs)
    return socket_creator


def create_connection_ipv4(address, timeout=None, source_address=None,
                           only_ports=None):
    """Attempts to mimick to :py:func:`socket.create_connection`, but
    connections are only made to IPv4 addresses.

    :param only_ports: If given, can be a list to limit which ports are
                       restricted to IPv4. Connections to all other ports may
                       be IPv6.

    """
    host, port = address
    if only_ports and port not in only_ports:
        return socket.create_connection(address, timeout, source_address)
    last_exc = None
    for res in socket.getaddrinfo(host, port, socket.AF_INET):
        _, _, _, _, sockaddr = res
        try:
            return socket.create_connection(sockaddr, timeout, source_address)
        except socket.error as exc:
            last_exc = exc
    if last_exc is not None:
        raise last_exc
    else:
        raise socket.error('getaddrinfo returns an empty list')


def create_listeners(address,
                     family=socket.AF_UNSPEC,
                     socktype=socket.SOCK_STREAM,
                     proto=socket.IPPROTO_IP):
    """Uses :func:`socket.getaddrinfo` to create listening sockets for
    available socket parameters. For example, giving *address* as
    ``('localhost', 80)`` on a system with IPv6 would return one socket bound
    to ``127.0.0.1`` and one bound to ``::1`.

    May also be used for ``socket.AF_UNIX`` with a file path to produce a
    single unix domain socket listening on that path.

    :param address: A ``(host, port)`` tuple to listen on.
    :param family: the socket family, default ``AF_UNSPEC``.
    :param socktype: the socket type, default ``SOCK_STREAM``.
    :param proto: the socket protocol, default ``IPPROTO_IP``.

    """
    if family == socket.AF_UNIX:
        sock = socket.socket(family, socktype, proto)
        _init_socket(sock, address)
        return [sock]
    elif not isinstance(address, tuple) or len(address) != 2:
        raise ValueError(address)
    flags = socket.AI_PASSIVE
    host, port = address
    listeners = []
    last_exc = None
    for res in socket.getaddrinfo(host, port, family, socktype, proto, flags):
        fam, typ, prt, _, sockaddr = res
        try:
            sock = socket.socket(fam, typ, prt)
            _init_socket(sock, sockaddr)
        except socket.error as exc:
            last_exc = exc
        else:
            listeners.append(sock)
    if last_exc and not listeners:
        raise last_exc
    return listeners


def _init_socket(sock, sockaddr):
    try:
        sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
    except socket.error:
        pass
    try:
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    except socket.error:
        pass
    sock.setblocking(0)
    sock.bind(sockaddr)
    if sock.type != socket.SOCK_DGRAM:
        sock.listen(socket.SOMAXCONN)


# vim:et:fdm=marker:sts=4:sw=4:ts=4
