# -*- coding: utf-8 -*-
"""
:mod:`net` -- This provides handy utiliy functions for network services.
==============================================================================

.. module:: evasion.common.net
   :platform: Unix, MacOSX, Windows
   :synopsis: This provides handy utiliy functions for network services.
.. moduleauthor:: Oisin Mulvihill <oisin.mulvihill@gmail.com>

.. data:: PORT_RETRIES the amount of times get_free_port and wait_for_ready
will retry for by default.

.. autoexception:: evasion.common.net.NoFreePort

.. autofunction:: evasion.common.net.get_free_port

.. autofunction:: evasion.common.net.wait_for_service

.. autofunction:: evasion.common.net.wait_for_ready

"""
import time
import socket
import urllib
import random
import logging
import httplib
import urlparse


def get_log():
    return logging.getLogger('evasion.common.net')


PORT_RETRIES = 40


class NoFreePort(Exception):
    """Raised when get_free_port was unable to find a free port to use."""


def free_port_range(start=2000, stop=10000):
    """Used by get_free_port() to generate a random TCP port to test.

    :param start: The start of the TCP port range (default: 2000).

    :param stop: The stop of the TCP port range (default: 10000).

    :returns: A random int in the range(start, stop)

    """
    return random.randint(start, stop)


class NoFreePort(Exception):
    """Raised by get_free_port when no available TCP port could be found."""


def get_free_port(exclude_ports=[], retries=PORT_RETRIES, fp=free_port_range):
    """Called to return a free TCP port that we can use.

    This function gets a random port between 2000 - 10000.
    A test is done to check if the port is free by attempting
    to use it. If its not free another port is checked

    :exclude_ports: This is a list of port numbers to
    exclude from using. This could be a list of numbers
    generated by previous calls to this function.

    :param retries: The amount of attempts to try finding
    a free port.

    :param fp: The free port range number generator.

    This returns a TCP port number.

    :returns: The free port number to use.

    """
    log = get_log()
    returned = 0
    free_port = 0

    while retries:
        retries -= 1
        free_port = fp()

        # Get a port thats not in the exclude list
        exclude_retries = len(exclude_ports)
        while not exclude_retries:
            free_port = fp()
            if free_port in exclude_ports:
                exclude_retries -= 1
            else:
                break

        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            s.bind(('', free_port))
            s.close()

        except socket.error:
            # port not free, retry.
            log.info(
                "getFreePort: port not free %s, retrying with another port." % (
                free_port
            ))

        else:
            returned = free_port

    if not returned:
        # Retries finished and no free port was found:
        raise NoFreePort("I can't get a free port after retrying!")

    log.info("getFreePort: Free Port %s." % returned)

    return returned


def wait_for_service(host, port, retries=0, retry_period=5.0):
    """Called to wait until a socket connection can be made to a remote service.

    :param host: IP/DNS of the service to connect to.

    :param port: TCP port number to connect to.

    :param retries: (default: 0)

    The number of attempts before giving up on connecting
    to the browser. If this is 0 then we will wait forever
    for the browser to appear.

    :param retry_period: (default: 5.0) The amount of seconds
    to wait before the next connection attempt.

    :returns:

        True: success
        Failed: failed to connect after maximum retries.

    """
    log = get_log()
    returned = False
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    retries = int(retries)
    retry_period = float(retry_period)

    def check():
        returned = False
        try:
            log.debug("wait_for_service: connecting to host<%s> port<%s>"% (
                host,
                port
            ))
            s.connect((host, port))
        except socket.error:
            pass # not ready yet.
        else:
            log.debug("wait_for_service: Success! Connected to <%s:%s>"% (
                host,
                port
            ))
            returned = True

        try:
            s.close()
        except:
            pass

        return returned

    if not retries:
        # Keep connecting until its present:
        while True:
            time.sleep(retry_period)
            is_connected = check()
            if is_connected:
                returned = is_connected
                break
    else:
        # Give up after 'retries' attempts:
        for i in range(0, retries):
            returned = check()
            if returned:
                # Success!
                break
            else:
                time.sleep(retry_period)

    return returned


def wait_for_ready(uri, retries=PORT_RETRIES):
    """Called to wait for a web application to respond to normal requests.

    This function will attempt a HEAD request if its
    supported, otherwise it will use GET.

    :param uri: the URI of the web application on which
    it will receive requests.

    :param retries: The amount of attempts to try finding
    a free port.

    :returns: True: the web app ready.

    """
    returned = False

    URI = uri
    # Work set up the connection for the HEAD request:
    o = urlparse.urlsplit(URI)
    conn = httplib.HTTPConnection(o.hostname, o.port)

    while retries:
        retries -= 1
        try:
            # Just get the headers and not the body to speed things up.
            conn.request("HEAD",'/')
            res = conn.getresponse()
            if res.status == httplib.OK:
                # success, its ready.
                returned = True
                break;

            elif res.status == httplib.NOT_IMPLEMENTED:
                # HEAD not supported try a GET instead:
                try:
                    urllib.urlopen(URI)
                except IOError:
                    # Not ready yet. I should check the exception to
                    # make sure its socket error or we could be looping
                    # forever. I'll need to use a state machine if this
                    # prototype works. For now I'm taking the "head in
                    # the sand" approach.
                    pass
                else:
                    # success, its ready.
                    returned = True
                    break;

        except httplib.CannotSendRequest:
            # Not ready yet.
            pass

        except socket.error:
            # Not ready yet. I should check the exception to
            # make sure its socket error or we could be looping
            # forever. I'll need to use a state machine if this
            # prototype works. For now I'm taking the "head in
            # the sand" approach.
            pass

        time.sleep(0.8)

    return returned
