# -*- coding: utf-8 -*-
"""Module grouping network-related utilities and functions."""

from queue import Empty, Queue
from threading import Thread

import requests
import urllib3
from requests.adapters import HTTPAdapter

import pydov

request_timeout = 300


class TimeoutHTTPAdapter(HTTPAdapter):
    """HTTPAdapter which adds a default timeout to requests. Allows timeout
    to be overridden on a per-request basis.
    """

    def __init__(self, *args, **kwargs):
        """Initialisation."""
        self.timeout = request_timeout
        if "timeout" in kwargs:
            self.timeout = kwargs["timeout"]
            del kwargs["timeout"]
        super().__init__(*args, **kwargs)

    def send(self, request, **kwargs):
        """Sends PreparedRequest object. Returns Response object.

        Parameters
        ----------
        request : requests.PreparedRequest
            The PreparedRequest being sent.

        Returns
        -------
        requests.Response
            The Response of the request.
        """
        timeout = kwargs.get("timeout")
        if timeout is None:
            kwargs["timeout"] = self.timeout
        return super().send(request, **kwargs)


class SessionFactory:
    """Class for generating pydov configured requests Sessions. They are used
    to send HTTP requests using our user-agent and with added retry-logic.

    One global session is used for all requests, and additionally one
    session is used per thread executing XML requests in parallel.
    """
    @staticmethod
    def get_session():
        """Request a new session.

        Returns
        -------
        requests.Session
            pydov configured requests Session.
        """
        session = requests.Session()

        session.headers.update(
            {'user-agent': 'pydov/{}'.format(pydov.__version__)})

        try:
            retry = urllib3.util.Retry(
                total=10, connect=10, read=10, redirect=5, backoff_factor=1,
                allowed_methods=set(
                    ['HEAD', 'GET', 'POST', 'PUT', 'OPTIONS']))
        except TypeError:
            # urllib3 < 1.26.0 used method_whitelist instead
            retry = urllib3.util.Retry(
                total=10, connect=10, read=10, redirect=5, backoff_factor=1,
                method_whitelist=set(
                    ['HEAD', 'GET', 'POST', 'PUT', 'OPTIONS']))

        adapter = TimeoutHTTPAdapter(timeout=request_timeout,
                                     max_retries=retry)
        session.mount('http://', adapter)
        session.mount('https://', adapter)
        return session


class LocalSessionThreadPool:
    """Thread pool of LocalSessionThreads used to perform XML I/O operations
    in parallel.
    """

    def __init__(self, workers=4):
        """Initialisation.

        Set up the pool and start all workers.

        Parameters
        ----------
        workers : int, optional
            Number of worker threads to use, defaults to 4.
        """
        self.workers = []
        self.input_queue = Queue(maxsize=100)
        self.result_queue = Queue()

        for i in range(workers):
            self.workers.append(LocalSessionThread(self.input_queue))

        self._start()

    def _start(self):
        """Start all worker threads. """
        for w in self.workers:
            w.start()

    def stop(self):
        """Stop all worker threads. """
        for w in self.workers:
            w.stop()

    def execute(self, fn, args):
        """Execute the given function with its arguments in a worker thread.

        This will add the job to the queue and will not wait for the result.
        Use join() to retrieve the result.

        Parameters
        ----------
        fn : function
            Function to execute.
        args : tuple
            Arguments that will be passed to the function.
        """
        r = WorkerResult()
        self.input_queue.put((fn, args, r))
        self.result_queue.put(r)

    def join(self):
        """Wait for all the jobs to be executed and return the results of all
        jobs in a list.

        Yields
        ------
        WorkerResult
            Results of the executed functions in the order they were
            submitted.
        """
        self.input_queue.join()
        self.stop()

        while not self.result_queue.empty():
            yield self.result_queue.get()


class WorkerResult:
    """Class for storing the result of a job execution in the result queue.

    This allows putting a result instance in the queue on job submission and
    fill in the result later when the job completes. This ensures the result
    output is in the same order as the jobs were submitted.
    """

    def __init__(self):
        """Initialisation. """
        self.result = None
        self.error = None

    def set_result(self, value):
        """Set the result of this job.

        Parameters
        ----------
        value : any
            The result of the execution of the job.
        """
        self.result = value

    def get_result(self):
        """Retrieve the result of this job.

        Returns
        -------
        any
            The result of the execution of the job.
        """
        return self.result

    def set_error(self, error):
        """Set the error, in case the jobs fails with an exception.

        Parameters
        ----------
        error : Exception
            The exception raised while executing this job.
        """
        self.error = error

    def get_error(self):
        """Retrieve the error, if any, of this job.

        Returns
        -------
        Exception
            The exception raised while executing this job.
        """
        return self.error


class LocalSessionThread(Thread):
    """Worker thread using a local Session to execute functions. """

    def __init__(self, input_queue):
        """Initialisation.

        Bind to the input queue and create a Session.

        Parameters
        ----------
        input_queue : queue.Queue
            Queue to poll for input, this should be in the form of a tuple with
            3 items: function to call, list with arguments and WorkerResult
            instance to store the output. The list with arguments will be
            automatically extended with the local Session instance.
        """
        super().__init__()
        self.input_queue = input_queue

        self.stopping = False
        self.session = SessionFactory.get_session()

    def stop(self):
        """Stop the worker thread at the next occasion. This can take up to
        500 ms. """
        self.stopping = True

    def run(self):
        """Executed while the thread is running. This is called implicitly
        when starting the thread. """
        while not self.stopping:
            try:
                fn, args, r = self.input_queue.get(timeout=0.5)
                args = list(args)
                args.append(self.session)

                try:
                    result = fn(*args)
                except BaseException as e:
                    r.set_error(e)
                else:
                    r.set_result(result)
                finally:
                    self.input_queue.task_done()
            except Empty:
                pass

[((64, 18, 64, 36), 'requests.Session', 'requests.Session', ({}, {}), '()', False, 'import requests\n'), ((104, 27, 104, 45), 'queue.Queue', 'Queue', (), '', False, 'from queue import Empty, Queue\n'), ((105, 28, 105, 35), 'queue.Queue', 'Queue', ({}, {}), '()', False, 'from queue import Empty, Queue\n')]