##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Adjustments are tunable parameters.
"""
import getopt
import socket

from waitress.compat import (
    PY2,
    WIN,
    string_types,
    HAS_IPV6,
    )

truthy = frozenset(('t', 'true', 'y', 'yes', 'on', '1'))

def asbool(s):
    """ Return the boolean value ``True`` if the case-lowered value of string
    input ``s`` is any of ``t``, ``true``, ``y``, ``on``, or ``1``, otherwise
    return the boolean value ``False``.  If ``s`` is the value ``None``,
    return ``False``.  If ``s`` is already one of the boolean values ``True``
    or ``False``, return it."""
    if s is None:
        return False
    if isinstance(s, bool):
        return s
    s = str(s).strip()
    return s.lower() in truthy

def asoctal(s):
    """Convert the given octal string to an actual number."""
    return int(s, 8)

def aslist_cronly(value):
    if isinstance(value, string_types):
        value = filter(None, [x.strip() for x in value.splitlines()])
    return list(value)

def aslist(value):
    """ Return a list of strings, separating the input based on newlines
    and, if flatten=True (the default), also split on spaces within
    each line."""
    values = aslist_cronly(value)
    result = []
    for value in values:
        subvalues = value.split()
        result.extend(subvalues)
    return result

def slash_fixed_str(s):
    s = s.strip()
    if s:
        # always have a leading slash, replace any number of leading slashes
        # with a single slash, and strip any trailing slashes
        s = '/' + s.lstrip('/').rstrip('/')
    return s

class _str_marker(str):
    pass

class _int_marker(int):
    pass

class Adjustments(object):
    """This class contains tunable parameters.
    """

    _params = (
        ('host', str),
        ('port', int),
        ('ipv4', asbool),
        ('ipv6', asbool),
        ('listen', aslist),
        ('threads', int),
        ('trusted_proxy', str),
        ('url_scheme', str),
        ('url_prefix', slash_fixed_str),
        ('backlog', int),
        ('recv_bytes', int),
        ('send_bytes', int),
        ('outbuf_overflow', int),
        ('inbuf_overflow', int),
        ('connection_limit', int),
        ('cleanup_interval', int),
        ('channel_timeout', int),
        ('log_socket_errors', asbool),
        ('max_request_header_size', int),
        ('max_request_body_size', int),
        ('expose_tracebacks', asbool),
        ('ident', str),
        ('asyncore_loop_timeout', int),
        ('asyncore_use_poll', asbool),
        ('unix_socket', str),
        ('unix_socket_perms', asoctal),
    )

    _param_map = dict(_params)

    # hostname or IP address to listen on
    host = _str_marker('0.0.0.0')

    # TCP port to listen on
    port = _int_marker(8080)

    listen = ['{}:{}'.format(host, port)]

    # mumber of threads available for tasks
    threads = 4

    # Host allowed to overrid ``wsgi.url_scheme`` via header
    trusted_proxy = None

    # default ``wsgi.url_scheme`` value
    url_scheme = 'http'

    # default ``SCRIPT_NAME`` value, also helps reset ``PATH_INFO``
    # when nonempty
    url_prefix = ''

    # server identity (sent in Server: header)
    ident = 'waitress'

    # backlog is the value waitress passes to pass to socket.listen() This is
    # the maximum number of incoming TCP connections that will wait in an OS
    # queue for an available channel.  From listen(1): "If a connection
    # request arrives when the queue is full, the client may receive an error
    # with an indication of ECONNREFUSED or, if the underlying protocol
    # supports retransmission, the request may be ignored so that a later
    # reattempt at connection succeeds."
    backlog = 1024

    # recv_bytes is the argument to pass to socket.recv().
    recv_bytes = 8192

    # send_bytes is the number of bytes to send to socket.send().  Multiples
    # of 9000 should avoid partly-filled packets, but don't set this larger
    # than the TCP write buffer size.  In Linux, /proc/sys/net/ipv4/tcp_wmem
    # controls the minimum, default, and maximum sizes of TCP write buffers.
    send_bytes = 18000

    # A tempfile should be created if the pending output is larger than
    # outbuf_overflow, which is measured in bytes. The default is 1MB.  This
    # is conservative.
    outbuf_overflow = 1048576

    # A tempfile should be created if the pending input is larger than
    # inbuf_overflow, which is measured in bytes. The default is 512K.  This
    # is conservative.
    inbuf_overflow = 524288

    # Stop creating new channels if too many are already active (integer).
    # Each channel consumes at least one file descriptor, and, depending on
    # the input and output body sizes, potentially up to three.  The default
    # is conservative, but you may need to increase the number of file
    # descriptors available to the Waitress process on most platforms in
    # order to safely change it (see ``ulimit -a`` "open files" setting).
    # Note that this doesn't control the maximum number of TCP connections
    # that can be waiting for processing; the ``backlog`` argument controls
    # that.
    connection_limit = 100

    # Minimum seconds between cleaning up inactive channels.
    cleanup_interval = 30

    # Maximum seconds to leave an inactive connection open.
    channel_timeout = 120

    # Boolean: turn off to not log premature client disconnects.
    log_socket_errors = True

    # maximum number of bytes of all request headers combined (256K default)
    max_request_header_size = 262144

    # maximum number of bytes in request body (1GB default)
    max_request_body_size = 1073741824

    # expose tracebacks of uncaught exceptions
    expose_tracebacks = False

    # Path to a Unix domain socket to use.
    unix_socket = None

    # Path to a Unix domain socket to use.
    unix_socket_perms = 0o600

    # The socket options to set on receiving a connection.  It is a list of
    # (level, optname, value) tuples.  TCP_NODELAY disables the Nagle
    # algorithm for writes (Waitress already buffers its writes).
    socket_options = [
        (socket.SOL_TCP, socket.TCP_NODELAY, 1),
    ]

    # The asyncore.loop timeout value
    asyncore_loop_timeout = 1

    # The asyncore.loop flag to use poll() instead of the default select().
    asyncore_use_poll = False

    # Enable IPv4 by default
    ipv4 = True

    # Enable IPv6 by default
    ipv6 = True

    def __init__(self, **kw):

        if 'listen' in kw and ('host' in kw or 'port' in kw):
            raise ValueError('host and or port may not be set if listen is set.')

        for k, v in kw.items():
            if k not in self._param_map:
                raise ValueError('Unknown adjustment %r' % k)
            setattr(self, k, self._param_map[k](v))

        if (not isinstance(self.host, _str_marker) or
           not isinstance(self.port, _int_marker)):
            self.listen = ['{}:{}'.format(self.host, self.port)]

        enabled_families = socket.AF_UNSPEC

        if not self.ipv4 and not HAS_IPV6: # pragma: no cover
            raise ValueError(
                'IPv4 is disabled but IPv6 is not available. Cowardly refusing to start.'
            )

        if self.ipv4 and not self.ipv6:
            enabled_families = socket.AF_INET

        if not self.ipv4 and self.ipv6 and HAS_IPV6:
            enabled_families = socket.AF_INET6

        wanted_sockets = []
        hp_pairs = []
        for i in self.listen:
            if ':' in i:
                (host, port) = i.rsplit(":", 1)

                # IPv6 we need to make sure that we didn't split on the address
                if ']' in port: # pragma: nocover
                    (host, port) = (i, str(self.port))
            else:
                (host, port) = (i, str(self.port))

            if WIN and PY2: # pragma: no cover
                try:
                    # Try turning the port into an integer
                    port = int(port)
                except:
                    raise ValueError(
                        'Windows does not support service names instead of port numbers'
                    )

            try:
                if '[' in host and ']' in host: # pragma: nocover
                    host = host.strip('[').rstrip(']')

                if host == '*':
                    host = None

                for s in socket.getaddrinfo(
                    host,
                    port,
                    enabled_families,
                    socket.SOCK_STREAM,
                    socket.IPPROTO_TCP,
                    socket.AI_PASSIVE
                ):
                    (family, socktype, proto, _, sockaddr) = s

                    # It seems that getaddrinfo() may sometimes happily return
                    # the same result multiple times, this of course makes
                    # bind() very unhappy...
                    #
                    # Split on %, and drop the zone-index from the host in the
                    # sockaddr. Works around a bug in OS X whereby
                    # getaddrinfo() returns the same link-local interface with
                    # two different zone-indices (which makes no sense what so
                    # ever...) yet treats them equally when we attempt to bind().
                    if (
                        sockaddr[1] == 0 or
                        (sockaddr[0].split('%', 1)[0], sockaddr[1]) not in hp_pairs
                    ):
                        wanted_sockets.append((family, socktype, proto, sockaddr))
                        hp_pairs.append((sockaddr[0].split('%', 1)[0], sockaddr[1]))
            except:
                raise ValueError('Invalid host/port specified.')

        self.listen = wanted_sockets

    @classmethod
    def parse_args(cls, argv):
        """Pre-parse command line arguments for input into __init__.  Note that
        this does not cast values into adjustment types, it just creates a
        dictionary suitable for passing into __init__, where __init__ does the
        casting.
        """
        long_opts = ['help', 'call']
        for opt, cast in cls._params:
            opt = opt.replace('_', '-')
            if cast is asbool:
                long_opts.append(opt)
                long_opts.append('no-' + opt)
            else:
                long_opts.append(opt + '=')

        kw = {
            'help': False,
            'call': False,
        }

        opts, args = getopt.getopt(argv, '', long_opts)
        for opt, value in opts:
            param = opt.lstrip('-').replace('-', '_')

            if param == 'listen':
                kw['listen'] = '{} {}'.format(kw.get('listen', ''), value)
                continue

            if param.startswith('no_'):
                param = param[3:]
                kw[param] = 'false'
            elif param in ('help', 'call'):
                kw[param] = True
            elif cls._param_map[param] is asbool:
                kw[param] = 'true'
            else:
                kw[param] = value

        return kw, args
