"""This module contains the class used to wrap a multithreaded function."""
from __future__ import annotations

import multiprocessing
import sys
import time
from typing import Callable

from timeoutd.exceptions import _raise_exception


def _target(queue, function, *args, **kwargs) -> None:
    """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 Exception:  # pylint: disable=broad-except
        queue.put((False, sys.exc_info()[1]))


class _Timeout:  # pylint: disable=too-many-instance-attributes
    """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.

    :param function: function to wrap
    :type function: Callable
    :param exception_type: exception to raise when the timeout is
        reached.
    :type exception_type: type
    :param exception_message: optional message to pass to the exception
        when the timeout is reached.
    :type exception_message: str
    :param limit: optional time limit in seconds or fractions of a
        second. If None is passed, no timeout is applied.
    :type limit: float
    """

    def __init__(
        self,
        function: Callable,
        exception_type: type,
        exception_message: str | None,
        limit: float | None,
    ):  # pylint: disable=too-many-arguments
        """Initialize instance in preparation for being called."""
        self.__limit = limit
        self.__function = function
        self.__exception_type = exception_type
        self.__exception_message = exception_message
        self.__name__ = function.__name__
        self.__doc__ = function.__doc__
        self.__timeout = time.time()
        self.__process = multiprocessing.Process()
        self.__queue: multiprocessing.Queue = multiprocessing.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 = multiprocessing.Queue(1)
        args = (self.__queue, self.__function) + args
        self.__process = multiprocessing.Process(
            target=_target, args=args, kwargs=kwargs
        )
        self.__process.daemon = True
        self.__process.start()
        if self.__limit is not None:
            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.__exception_type, self.__exception_message)

    @property
    def ready(self):
        """Read-only property indicating status of "value" property."""
        if self.__limit and 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
        return None
