# -*- coding: utf-8 -*-

import multiprocess
import time
from functools import wraps

# Comes from https://github.com/pnpnpn/timeout-decorator/blob/master/
# timeout_decorator/timeout_decorator.py. Since we were not using the whol0
# module and it was in a beta state we just used the multithreaded management.


def _raise_exception(exception, exception_message):
    """ This function checks if a exception message is given.
    If there is no exception message, the default behaviour is maintained.
    If there is an exception message, the message is passed
    to the exception with the 'value' keyword.
    """
    if exception_message is None:
        raise exception()
    else:
        raise exception(value=exception_message)


def _target(queue, function, *args, **kwargs):
    """Run a function with arguments and return output via a queue.
    This is a helper function for the Process created in _Timeout. It runs
    the function with positional arguments and keyword arguments and then
    returns the function's output by way of a queue. If an exception gets
    raised, it is returned to _Timeout to be raised by the value property.
    """
    try:
        queue.put((True, function(*args, **kwargs)))
    except:
        queue.put((False, sys.exc_info()[1]))


class _Timeout(object):

    """Wrap a function and add a timeout (limit) attribute to it.
    Instances of this class are automatically generated by the add_timeout
    function defined above. Wrapping a function allows asynchronous calls
    to be made and termination of execution after a timeout has passed.
    """

    def __init__(self, function, timeout_exception, exception_message, limit):
        """Initialize instance in preparation for being called."""
        self.__limit = limit
        self.__function = function
        self.__timeout_exception = timeout_exception
        self.__exception_message = exception_message
        self.__name__ = function.__name__
        self.__doc__ = function.__doc__
        self.__timeout = time.time()
        self.__process = multiprocess.Process()
        self.__queue = multiprocess.Queue()

    def __call__(self, *args, **kwargs):
        """Execute the embedded function object asynchronously.
        The function given to the constructor is transparently called and
        requires that "ready" be intermittently polled. If and when it is
        True, the "value" property may then be checked for returned data.
        """
        self.__limit = kwargs.pop('timeout', self.__limit)
        self.__queue = multiprocess.Queue(1)
        args = (self.__queue, self.__function) + args
        self.__process = multiprocess.Process(target=_target,
                                              args=args,
                                              kwargs=kwargs)
        self.__process.daemon = True
        self.__process.start()
        self.__timeout = self.__limit + time.time()
        while not self.ready:
            time.sleep(0.01)
        return self.value

    def cancel(self):
        """Terminate any possible execution of the embedded function."""
        if self.__process.is_alive():
            self.__process.terminate()

        _raise_exception(self.__timeout_exception, self.__exception_message)

    @property
    def ready(self):
        """Read-only property indicating status of "value" property."""
        if self.__timeout < time.time():
            self.cancel()
        return self.__queue.full() and not self.__queue.empty()

    @property
    def value(self):
        """Read-only property containing data returned from function."""
        if self.ready is True:
            flag, load = self.__queue.get()
            if flag:
                return load
            raise load


def timeout(seconds=None, timeout_exception=TimeoutError,
            exception_message=None):
    """
    Timeout decorator. This comes handy when network resolution is an issue
    and timeout is not handled by the connectivity layer.

    Keyword arguments:
    seconds -- Int, timeout threshold in seconds
    timeout_exception -- Exception, the exception raises if timeout occurs
    exception_message -- String, error message to display
    """
    def decorator(func):
        @wraps(func)
        def new_function(*args, **kwargs):
            timeout_wrapper = _Timeout(
                func, timeout_exception, exception_message, seconds)
            return timeout_wrapper(*args, **kwargs)
        return new_function
    return decorator
