"""A few helper decorators to simplify construction of the code."""
import functools
import inspect
import typing

from .exceptions import ArgumentTypeError


def check_simple_types(f: typing.Callable) -> typing.Callable:
    """A decorator that will check the types of the arguments at runtime.

    This has a limitation that it can only deal with a concrete number of
    positional arguments, each of which is annotated with a concrete type.
    This means that no fancy `typing` annotations will be supported.
    """
    spec = inspect.getfullargspec(f)
    annotations = [[name, spec.annotations[name]] for name in spec.args]

    @functools.wraps(f)
    def ret(*args):
        for i, arg in enumerate(args):
            if not isinstance(arg, annotations[i][1]):
                fmt = "Expecting {name} to be of type {typ}, was {realtyp} instead."
                raise ArgumentTypeError(fmt.format(name=annotations[i][0], typ=annotations[i][1], realtyp=type(arg)))
        return f(*args)

    return ret


def wrap_exceptions_with(ex: type(Exception), message='', target=Exception):
    """Catch exceptions and rethrow them wrapped in an exception of our choosing.

    This is mainly for the purpose of catching whatever builtin exceptions might be thrown and showing them to the
    outside world as custom module exceptions. That way an external importer can just catch the RollError and thus catch
    every exception thrown by this module.

    ``target`` specifies what to catch and therefore wrap. By default it's ``Exception`` to cast a wide net but note
    that you can catch any specific exception you please, or a set of exceptions if you pass in a tuple of exception
    classes.

    And yes, this is just try/catch/rethrow. It's more decorative this way though. Plus saves you some indentation as
    you're trying to wrap an entire function.

    :param ex: The exception to use as wrapper.
    :param message: A custom message to use for the wrapper exception.
    :param target: Which exception(s) you want to catch.
    """
    def decorator(f):
        @functools.wraps(f)
        def wrapped(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except target as e:
                raise ex(message) from e

        return wrapped

    return decorator
