import functools
import inspect
import warnings
import logging

__all__ = ['keyword_deprecation',
           'keyword_equivalence',
           'add_method_to_instance',
           'add_method_to_class',
           'add_prop_to_instance',
           'add_prop_to_class',
           'deprecated']

def keyword_equivalence(func=None, *, this_or_that):
    """
    Keyword equivalences decorator.

    Parameters
    ----------
    this_or_that : dict
        Dictionary of equivalent kwargs, {'canonical': ['alt1', 'alt2', 'alt3']}

    Examples
    --------
    @keyword_equivalence(this_or_that={'data':'time', 'series_ids':['unit_ids', 'cell_ids', 'neuron_ids']})
    def myfunc(arg1, arg2, *, data=None, unit_ids=5):
        ...

    @keyword_equivalence(this_or_that={'n_intervals':'n_epochs'})
    def partition(n_intervals=None, n_samples=None):
        ...

    """
    def _decorate(function):
        @functools.wraps(function)
        def wrapped_function(*args, **kwargs):
            for canonical, equiv in this_or_that.items():
                canonical_val = kwargs.pop(canonical, None)
                if isinstance(equiv, list):
                    equiv_val = None
                    count = 0
                    alt = []
                    for ee in equiv:
                        temp_val = kwargs.pop(ee, None)
                        if canonical_val is not None and temp_val is not None:
                            raise ValueError("Cannot pass both '{}' and '{}'. Use '{}' instead.".format(canonical, ee, canonical))
                        if temp_val is not None:
                            equiv_val = temp_val
                            count += 1
                            alt.append(ee)
                        if count > 1:
                            raise ValueError("Cannot pass both '{}' and '{}'. Use '{}' instead.".format(alt[0], alt[1], canonical))
                elif isinstance(equiv, str):
                    equiv_val = kwargs.pop(equiv, None)
                    if canonical_val is not None and equiv_val is not None:
                        raise ValueError("Cannot pass both '{}' and '{}'. Use '{}' instead.".format(canonical, ee, canonical))
                else:
                    raise TypeError('unknown equivalence kwarg type')
                if equiv_val is not None:
                    kwargs[canonical] = equiv_val
                else:
                    kwargs[canonical] = canonical_val

            return function(*args, **kwargs)
        return wrapped_function
    if func:
        return _decorate(func)

    return _decorate

def keyword_deprecation(func=None, *, replace_x_with_y=None):
    """
    Keyword deprecator.

    If you have a function with keywords kw1 and kw2 that you want to replace
    or update to nkw1 and nkw2, then this decorator can be used to support the
    transition. In particular, you should modify your function to use only the
    new keywords (both in the function definition and body), and then use this
    decorator to support calling the function with the previous keywords, kw1
    and kw2. This decorator assumes a one-to-one mapping between old and new
    keywords, so essentially it is used when a keyword is renamed for clarity.

    Parameters
    ----------
    replace_x_with_y : dict
        Dictionary of kwargs to replace, {'old': 'new'}

    Example
    -------
    @keyword_deprecation(replace_x_with_y={'old1':'new1', 'old2':'new2'})
    def myfunc(arg1, arg2, *, new1=None, new2=5):
        pass
    """
    def _decorate(function):
        @functools.wraps(function)
        def wrapped_function(*args, **kwargs):
            if replace_x_with_y is not None:
                for oldkwarg, newkwarg in replace_x_with_y.items():
                    newvalue = kwargs.pop(newkwarg, None)
                    oldvalue = kwargs.pop(oldkwarg, None)
                    if newvalue is not None and oldvalue is not None:
                        raise ValueError("Cannot pass both '{}' and '{}'. Use '{}' instead.".format(oldkwarg, newkwarg, newkwarg))
                    if oldvalue is not None:
                        logging.warn("'{}' has been deprecated, use '{}' instead.".format(oldkwarg, newkwarg))
                        kwargs[newkwarg] = oldvalue
                    else:
                        kwargs[newkwarg] = newvalue
            return function(*args, **kwargs)
        return wrapped_function
    if func:
#         print('no args in decorator')
        return _decorate(func)

    return _decorate

def deprecated(func):
    '''This is a decorator which can be used to mark functions
    as deprecated. It will result in a warning being emitted
    when the function is used.'''

    @functools.wraps(func)
    def new_func(*args, **kwargs):
        warnings.warn_explicit(
            "Call to deprecated function {}.".format(func.__name__),
            category=DeprecationWarning,
            filename=func.__code__.co_filename,
            lineno=func.__code__.co_firstlineno + 1
        )
        return func(*args, **kwargs)
    new_func.__name__ = func.__name__
    new_func.__doc__ = func.__doc__
    new_func.__dict__.update(func.__dict__)
    return new_func

# ## Usage examples ##
# @deprecated
# def my_func():
#     pass

# @other_decorators_must_be_upper
# @deprecated
# def my_func():
#     pass

def add_method_to_instance(instance):
    """Add a method to an object instance.

    Example
    -------

    >>> class Foo:
    >>> def __init__(self):
    >>>     self.x = 42

    >>> foo = Foo()

    >>> @add_method_to_instance(foo)
    >>> def print_x(self):
    >>>     \"""hello\"""
    >>>     print(self.x)

    """
    if inspect.isclass(instance):
        raise TypeError("instance expected, class object received")
    def decorator(f):
        import types
        f = types.MethodType(f, instance)
        setattr(instance, f.__name__, f)
        return f
    return decorator

def add_method_to_class(cls):
    """working for both class and instance inputs"""
    if not inspect.isclass(cls):
        cls = type(cls)
    def decorator(f):
        if not hasattr(cls, '__perinstance'):
            cls.__perinstance = True
        setattr(cls, f.__name__, f)
        return f
    return decorator

def add_prop_to_instance(instance):
    """working"""
    if inspect.isclass(instance):
        raise TypeError("instance expected, class object received")
    def decorator(f):
        cls = type(instance)
        cls = type(cls.__name__, (cls,), {})
        if not hasattr(cls, '__perinstance'):
            cls.__perinstance = True
        instance.__class__ = cls
        setattr(cls, f.__name__, property(f))
        return f
    return decorator

def add_prop_to_class(cls):
    """working"""
    if not inspect.isclass(cls):
        raise TypeError("class expected!")
    def decorator(f):
        if not hasattr(cls, '__perinstance'):
            cls.__perinstance = True
        setattr(cls, f.__name__, property(f))
        return f
    return decorator