"""
This code is borrowed from scikit-learn:
https://github.com/scikit-learn/scikit-learn/blob/master/\
        sklearn/utils/deprecation.py
"""
import sys
import warnings
import functools

__all__ = ["deprecated", "DeprecationDict"]


class deprecated(object):
  """Decorator to mark a function or class as deprecated.

  Issue a warning when the function is called/the class is instantiated and
  adds a warning to the docstring.

  The optional extra argument will be appended to the deprecation message
  and the docstring. Note: to use this with the default value for extra, put
  in an empty of parentheses:

  >>> from lanfang.utils import deprecated
  >>> deprecated() # doctest: +ELLIPSIS
  <lanfang.utils.deprecation.deprecated object at ...>

  >>> @deprecated()
  ... def some_function(): pass

  Parameters
  ----------
  extra : string
      to be added to the deprecation messages
  """
  def __init__(self, extra=''):
    self._m_extra = extra

  def __call__(self, obj):
    """Call method

    Parameters
    ----------
    obj : object
    """
    if isinstance(obj, type):
      return self._decorate_class(obj)
    else:
      return self._decorate_func(obj)

  def _decorate_class(self, cls):
    msg = "Class %s is deprecated" % cls.__name__
    if self._m_extra:
      msg += "; %s" % self._m_extra

    # FIXME: we should probably reset __new__ for full generality
    init = cls.__init__
    def wrapped(*args, **kwargs):
      warnings.warn(msg, category=DeprecationWarning)
      return init(*args, **kwargs)
    cls.__init__ = wrapped

    wrapped.__name__ = '__init__'
    wrapped.__doc__ = self._update_doc(init.__doc__)
    wrapped.deprecated_original = init
    return cls

  def _decorate_func(self, func):
    """Decorate function func"""
    msg = "Function %s is deprecated" % func.__name__
    if self._m_extra:
      msg += "; %s" % self._m_extra

    @functools.wraps(func)
    def wrapped(*args, **kwargs):
      warnings.warn(msg, category=DeprecationWarning)
      return func(*args, **kwargs)

    wrapped.__doc__ = self._update_doc(wrapped.__doc__)
    # Add a reference to the wrapped function so that we can introspect
    # on function arguments in Python 2 (already works in Python 3)
    wrapped.__wrapped__ = func
    return wrapped

  def _update_doc(self, olddoc):
    newdoc = "DEPRECATED"
    if self._m_extra:
      newdoc = "%s: %s" % (newdoc, self._m_extra)
    if olddoc:
      newdoc = "%s\n\n%s" % (newdoc, olddoc)
    return newdoc


def _is_deprecated(func):
  """Helper to check if func is wraped by our deprecated decorator"""
  if sys.version_info < (3, 5):
    raise NotImplementedError("This is only available for python3.5 "
                  "or above")
  closures = getattr(func, '__closure__', [])
  if closures is None:
    closures = []
  is_deprecated = ('deprecated' in ''.join([c.cell_contents
                        for c in closures
           if isinstance(c.cell_contents, str)]))
  return is_deprecated


class DeprecationDict(dict):
  """A dict which raises a warning when some keys are looked up

  Note, this does not raise a warning for __contains__ and iteration.

  It also will raise a warning even after the key has been manually set by
  the user.
  """
  def __init__(self, *args, **kwargs):
    self._deprecations = {}
    super(DeprecationDict, self).__init__(*args, **kwargs)

  def __getitem__(self, key):
    if key in self._deprecations:
      warn_args, warn_kwargs = self._deprecations[key]
      warnings.warn(*warn_args, **warn_kwargs)
    return super(DeprecationDict, self).__getitem__(key)

  def get(self, key, default=None):
    """Return the value corresponding to key, else default.

    Parameters
    ----------
    key : any hashable object
      The key
    default : object, optional
      The default returned when key is not in dict
    """
    # dict does not implement it like this, hence it needs to be overridden
    try:
      return self[key]
    except KeyError:
      return default

  def add_warning(self, key, *args, **kwargs):
    """Add a warning to be triggered when the specified key is read

    Parameters
    ----------
    key : any hashable object
      The key
    """
    self._deprecations[key] = (args, kwargs)
