"""Implementation of autosig."""
from attr import attrib, NOTHING, fields_dict, make_class
from collections import OrderedDict
from functools import wraps
from inspect import getsource, signature
from itertools import chain
import re
from toolz.functoolz import curry
from types import BuiltinFunctionType

__all__ = ["Signature", "autosig", "param", "Retval"]

AUTOSIG_DOCSTRING = "__autosig_docstring__"
AUTOSIG_POSITION = "__autosig_position__"


def always_valid(x):
    return True


def identity(x):
    return x


class Retval:
    """Define return values in a Signature class.

    Parameters
    ----------
    validator : callable or type
        If a callable, it takes the return value as an argument, raising an exception or returning False if invalid; returning True otherwise. If a type, the return value must be an instance of that type.
    converter : callable
        The callable is executed with the return value as an argument and its return value is returned instead. Useful to enforce properties of return values, e.g. type, but not only.
    docstring : string
        The content for the docstring Returns section.


    """

    def __init__(self, validator=always_valid, converter=identity, docstring=""):
        """See class docs."""
        self._validator = check(validator, is_retval=True)
        self._converter = converter
        self._docstring = docstring

    def __call__(self, x):
        """Execute converter and validator with x as argument.

        Returns converter(x) if validator and converter succeed, raises an exception otherwise.

        Parameters
        ----------
        x : Any
            The return value of the autosig-decorated function.

        Returns
        -------
        Any
            The return value of converter(x).

        """
        x = self._converter(x)
        self._validator(x)
        return x


def param(
    default=NOTHING,
    validator=always_valid,
    converter=identity,
    docstring="",
    position=-1,
    kw_only=False,
):
    """Define parameters in a signature class.

    Parameters
    ----------
    default : Any
        The default value for the parameter (defaults to no default, that is, mandatory).
    validator : callable or type
        If a callable, it takes the actual parameter as an argument, raising an exception or returning False if invalid; returning True otherwise. If a type, the actual parameter must be instance of that type.
    converter : callable
        The callable is executed with the parameter as an argument and its value assigned to the parameter itself. Useful for type conversions, but not only (e.g. truncate range of parameter).
    docstring : string
        The docstring fragment for this parameter.
    position : int
        Desired position of the param in the signature. Negative values start from the end.
    kw_only : bool
        Whether to make this parameter keyword-only.


    Returns
    -------
    attr.Attribute
        Object describing all the properties of the parameter. Can be reused in multiple signature definitions to enforce consistency.

    """
    validator = check(validator, is_retval=False)
    metadata = {AUTOSIG_DOCSTRING: docstring, AUTOSIG_POSITION: position}
    kwargs = locals()
    for key in ("docstring", "position"):
        del kwargs[key]
    return attrib(**kwargs)


@curry
def keyfun(x, l):
    pos = x[1].metadata[AUTOSIG_POSITION]
    return pos if pos >= 0 else l + pos


class Signature:
    r"""Class to represent signatures.

    Parameters
    ----------
    \*params : (str, attr.Attribute)
        Optional first non-pair argument describes the return value.
        Each following argument is a pair with the name of an argument in the signature and a description of it generated with a call to param.
    \*\*kwparams : attr.Attribute
        Each keyword argument becomes an argument named after the key in the signature of a function and must be initialized with a param call. Requires python >=3.6. If both *param and **params are provided the first will be concatenated with items of the second, in this order.

    Returns
    -------
    Signature
        The object created.

    """

    def __init__(self, *params, **kwparams):
        """See class docs."""
        # grab retval id any
        if len(params) > 0 and isinstance(params[0], Retval):
            self._retval = params[0]
            params = params[1:]
        else:
            self._retval = None

        assert all(map(lambda x: len(x) == 2, params)), "Non keyword args must be pairs"
        all_params = list(chain(iter(params), kwparams.items()))
        self._params = OrderedDict(sorted(all_params, key=keyfun(l=len(all_params))))
        self._late_init = identity

    def __add__(self, other):
        """Combine signatures.

        The resulting signature has the union of the arguments of the left and right operands. The order is determined by the position property of the parameters and when there's a tie, positions are stably sorted with the left operand coming before the right one. Once a name clash occurs, the right operand, quite arbitrarily, wins. Please do not rely on this behavior, it may change.
        """

        assert (
            self._retval is None
            or other._retval is None
            or self._retval == other._retval
        )
        # must return compatible retvals to combine, or at most one of the two returns anything
        retval = self._retval if self._retval is not None else other._retval
        retval = [retval] if retval is not None else []
        return Signature(
            *(chain(retval, self._params.items(), other._params.items()))
        ).set_late_init(
            lambda param_dict: (
                self._late_init(param_dict),
                other._late_init(param_dict),
            )
        )

    def set_late_init(self, init):
        """Set a function to be called immediately after all arguments have been initialized.

        Use this function to perform initialization logic that involves multiple arguments in the signature.

        Parameters
        ----------
        init : FunctionType
            The init function is called after the initialization of all arguments in the signature but before the execution of the body of a function with that signature and is passed as an argument a dictionary with all arguments of the function. Returns None and acts exclusively by side effects.

        Returns
        -------
        Signature
            Returns self.

        """
        self._late_init = init
        return self

    def __call__(self, f):
        """Decorate function f with signature.

        Makes class directly usable as decorator

        Parameters
        ----------
        f : Function or method
            Function or method to be decorated.

        Returns
        -------
        Function
            A function decorated with this signature executing and returning values returned by f.

        """
        return autosig(self)(f)


def make_sig_class(sig):
    return make_class(
        "Sig_" + str(abs(hash(sig))),
        attrs=sig._params,
        # bases=(SigBase, ),
        eq=False,
        order=False,
    )


def autosig(sig_or_f):
    """Decorate  functions or methods to attach signatures.

        Use with (W) or without (WO) an argument::

            @autosig(Signature(a = param(), b=param()))
            def fun(a, b)

        or, equivlently (WO)::

            @autosig
            def fun(a=param(), b=param())

        Do not include the self argument in the signature when decorating
        methods



    Parameters
    ----------
    sig_or_f : Signature or function
        An instance of class Signature (W) or a function or method (WO) whose
        arguments are intialized with a call to param.

    Returns
    -------
    function
        A decorator (W) or an already decorated function (WO)
        The decorated function, will intialize, convert, and
        validate its arguments and will include argument docstrings
        in its docstring.

    """
    argument_deco = isinstance(sig_or_f, Signature)
    Sig = make_sig_class(
        (
            sig_or_f
            if argument_deco
            else Signature(
                *[(k, v.default) for k, v in signature(sig_or_f).parameters.items()]
            )
        )
    )
    retval_sig = (
        sig_or_f._retval
        if argument_deco and sig_or_f._retval is not None
        else lambda x: x
    )
    retval_docstring = (
        sig_or_f._retval._docstring
        if argument_deco and sig_or_f._retval is not None
        else ""
    )

    def decorator(f):
        # if decorator used on instance method, f is still a func here
        # hence I special case self arg in the following
        # will break if regular func has self arg TODO: fix
        if argument_deco:
            f_params = dict(signature(f).parameters)
            f_params.pop("self", None)
            Sig_params = signature(Sig).parameters
            assert f_params == Sig_params, "\n".join(
                [
                    "Mismatched signatures:",
                    str(f),
                    str(f_params),
                    str(Sig),
                    str(Sig_params),
                ]
            )  # compared as OrderedDicts, retval ignored TODO: support retval?

        @wraps(f)
        def wrapped(*args, **kwargs):
            try:
                bound_args = signature(f).bind(*args, **kwargs).arguments
                args_wo_self = bound_args.copy()
                args_wo_self.pop("self", None)
                params = Sig(**args_wo_self)
            except TypeError as te:
                raise TypeError(re.sub("__init__", f.__qualname__, te.args[0]))
            param_dict = params.__dict__
            if argument_deco:
                sig_or_f._late_init(param_dict)
            if "self" in bound_args:
                param_dict["self"] = bound_args["self"]
            retval = f(**param_dict)
            return retval_sig(retval)

        wrapped.__doc__ = (
            wrapped.__doc__
            or """Short summary.


            """
        )
        wrapped.__doc__ += "\n\nParameters\n---------\n" + "\n".join(
            [
                k + ": " + v.metadata[AUTOSIG_DOCSTRING]
                for k, v in fields_dict(Sig).items()
            ]
        )
        wrapped.__doc__ += "\n\nReturns\n-------\n" + (
            retval_docstring
            if retval_docstring
            else """     type
                        Description of returned object.
                """
        )
        return wrapped

    return decorator if argument_deco else decorator(sig_or_f)


def check(type_or_predicate, is_retval):
    """Transform a type or predicate into a autosig-friendly validator.

    Parameters
    ----------
    type_or_predicate : type or callable
        A type or a single argument function returning a bool, indicating whether the check was passed. The function will be passed an argument value when check(function) is used as validator argument to param.

    Returns
    -------
    Callable
        A Callable to be used as validator argument to param.

    """
    is_type = isinstance(type_or_predicate, type)
    predicate = (
        lambda x: isinstance(x, type_or_predicate) if is_type else type_or_predicate
    )

    def msg(name, value):
        predicate_desc = (
            type_or_predicate
            if is_type
            else (
                type_or_predicate.__qualname__
                if type_or_predicate.__qualname__ != "<lambda>"
                else getsource(type_or_predicate)
            )
        )
        m = (
            "type of {name} = {value} should be {predicate_desc}, {type} found instead"
            if is_type
            else "{name} = {value} should satisfy {predicate_desc}"
        )
        return m.format(
            name=name, value=value, type=type(value), predicate_desc=predicate_desc
        )

    def f_param(_, attribute=None, x=None):
        assert predicate(x), msg(name=attribute.name, value=x)

    def f_retval(x):
        assert predicate(x), msg(name="return value", value=x)

    return f_retval if is_retval else f_param
