# -*- coding: utf-8 -*-

#########################################################################
# pyeole.decorator - decorators with optional arguments
# Copyright © 2012 Pôle de Compétence EOLE <eole@ac-dijon.fr>
#
# License CeCILL:
#  * in french: http://www.cecill.info/licences/Licence_CeCILL_V2-fr.html
#  * in english http://www.cecill.info/licences/Licence_CeCILL_V2-en.html
#########################################################################

"""Decorators with optional arguments

The abstact :class:`EoleDecorator` class eases the creation of
decorator classes with optional arguments.

The :class:`advice` decorator permits to run functions before,
around and after a decorated function.

The :class:`deprecated` decorator permits to mark a function
deprecated with caller informations.

Implementation is inpired by:

- `Introduction to python decorators
  <http://www.artima.com/weblogs/viewpost.jsp?thread=240808>`_

- `Decorator Arguments
  <http://www.artima.com/weblogs/viewpost.jsp?thread=240845>`_

"""

import warnings
import logging

from functools import wraps

from datetime import date, timedelta

from pyeole.inspect_utils import get_caller_infos, format_caller


class EoleDecorator(object):
    """Abstact decorator class.

    When the subclass decorator has optional arguments, the user can
    avoid braces when none are supplied.

    Subclass decorators must implement a :meth:`decorate` method
    implementing the decorator with:

    - prototype: ``decorate(*args, **kwargs)`` with ``*args`` and
      ``**kwargs`` arguments of the decorated function

    - returns the result of the call of the decorated function
      accessible in attribute :attr:`self.decorated`.

    When the decorator is called without braces:

    - the decorated function is passed as the only argument to
      :meth:`__init__`

    - the :meth:`decorate` decorator is run at
      :meth:`EoleDecorator.__call__` time with arguments for the
      decorated function.

    When the decorator is called with braces and optional options:

    - decorator options are passed to :meth:`__init__`

    - the decorated function is passed as the only argument to
      :meth:`EoleDecorator.__call__`

    - the :meth:`decorate` decorator is not run at
      :meth:`EoleDecorator.__call__` time but returned to the caller
      in a wrapper.

    If a subclass decorator accepts arguments, it must define a custom
    :meth:`__init__` with:

    - the prototype ``__init__(self, <argument>=<default>, ...)``

    - check if first positional argument is callable:

      * if it's callable:

        + no other arguments are passed
        + call ``EoleDecorator.__init__(self, decorated=<argument>)``

      * if not:

        + store arguments in ``self`` for :meth:`decorate` use
        + call ``EoleDecorator.__init__(self)``

    ::

      @decorator_with_mandatory_option("foo")
      def some_dummy_function()
          pass

      @decorator_with_mandatory_option(mandatory_option="foo")
      def some_dummy_function()
          pass

      @decorator_with_optional_option("foo")
      def some_dummy_function()
          pass

      @decorator_with_optional_option(optional_option="foo")
      def some_dummy_function()
          pass

      @decorator_with_optional_option()
      def some_dummy_function()
          pass

      @decorator_with_optional_option
      def some_dummy_function()
          pass

    """

    def __init__(self, decorated=None):
        """Initialize the decorator.

        This is mostly due to subclasses badly handling optional
        positional parameters, look at :class:`deprecated` for how
        to handle them correctly or :class:`advice` for how to
        force keyword parameters only.

        :param decorated: function or method to decorate
        :type decorated: `function` or `instancemethod`
        :raise ValueError: if the optional first argument is not
                           callable.

        """
        self.decorated = None
        self._with_decorator_args = True

        # decorated is used when using "@decorator" without braces
        if callable(decorated):
            self.decorated = decorated
            self._with_decorator_args = False
            # Hide decorator
            self.__name__ = decorated.__name__
            self.__doc__ = decorated.__doc__
        elif decorated is not None:
            msg = u"Argument 'decorated' is not callable: '{0}'."
            raise ValueError(msg.format(decorated))

    @staticmethod
    def decorate(*args, **kwargs):
        """Wrapper around the decorated function.

        This method must be implemented by subclasses of
        :class:`EoleDecorator` and the decorated function available as
        :attr:`self.decorated` and returns its returned value.

        The stacklevel of the caller of the decorated function or
        methode in :meth:`decorate` is `3`.

        :param args: arguments for the decorated function or method
        :type args: `tuple`
        :param kwargs: keyword arguments for the decorated function or
                       method
        :type kwargs: `dict`
        :raise NotImplementedError: Must be implemented by subclasses

        """
        raise NotImplementedError(u"Undefined method 'decorate()'.")

    def _wrapp_decorate(self, decorated):
        """Return wrapped :meth:`self.decorate`

        It set properties of the wrapper to the decorated function
        ones and returns it to the caller since the
        :meth:`EoleDecorator.__call__` is done.

        :param decorated: function or method to decorate
        :type decorated: `function` or `instancemethod`
        :return: the wrapper to the :meth:`decorate` method
        :rtype: `function`

        """
        if self.decorated is None:
            self.decorated = decorated

        @wraps(decorated)
        def wrapper(*args, **kwargs):
            """Wrapp the decorated function
            """
            return self.decorate(*args, **kwargs)

        return wrapper

    def __call__(self, decorated=None, *args, **kwargs):
        """Call or return wrapper method.

        If the decorator is used without argument, then call the
        :meth:`decorate` method directly, return a wrapper
        otherwise.

        """
        if self._with_decorator_args:
            return self._wrapp_decorate(decorated)
        else:
            arglist = []
            if decorated is not None:
                arglist.append(decorated)
            arglist.extend(args)
            return self.decorate(*arglist, **kwargs)


class advice(EoleDecorator):
    """Execute code before, after and around decorated function.

    This is known as *method modifiers*, *hook* or *advice*.

    There are 3 modifiers:

      - :attr:`before(decorated, *args, **kwargs)`

      - :attr:`around(decorated, *args, **kwargs)`

      - :attr:`after(ret, decorated, *args, **kwargs)`

    They all receive as argument the decorated function and its
    arguments.

    The :attr:`after` modifier receives in addition, the value to
    return as first argument.

    The :attr:`around` modifier is more powerful, when provided, it
    is responsible of calling the decorated function (or not) and
    returning its possibly modified returned value (or not).

    The modifiers can be list of callable, in that case:

      - :attr:`before` modifiers are run sequentially in reversed
        order

      - :attr:`around` modifiers are run in nested reversed order with
        parameters:

        * :meth:`_call_around` as first parameter to nest
          :attr:`around` calls

        * the remaining list of :attr:`around` modifiers to run as
          second parameter, used by :meth:`_call_around`

        * the decorated function as third parameter

        * the arguments for the decorated functions

      - :attr:`after` modifiers are run in normal order

      - the returned value is the one of the last added
        :attr:`around` modifier.

    Here is a little call graph::

      before 2
        before 1
          around 2
            around 1
               decorated function
            around 1
          around 2
        after 1
      after 2

    For more explanations on the idea of advice, you can read:

      - `Wikipedia page on advice
        <https://en.wikipedia.org/wiki/Advice_(programming)>`_

      - up to section "before, after and around" in the perl
        `Moose::Manual::MethodModifiers
        <http://search.cpan.org/perldoc?Moose::Manual::MethodModifiers>`_

      - `GNU Emacs function advising
        <https://www.gnu.org/software/emacs/manual/html_node/elisp/Advising-Functions.html#Advising-Functions>`_

    """

    def __init__(self, faulty_positional=None, before=None,
                 around=None, after=None, decorated=None):
        """__init__(before=None, around=None, after=None)

        Accept only keyword parameters, the :data:`faulty_positional`
        argument is here to detect if positional argument was passed.

        If subclasses of :class:`advice` want to handle positional
        parameters, they must define a custom :meth:`__init__` with:

          - the prototype ``__init__(self, <argument>=<default>, ...)``

          - check if first positional argument is callable:

            * if it's callable:

              + no other arguments are passed
              + call ``advice.__init__(self, decorated=<argument>)``

            * if not:

              + store arguments in ``self`` for :meth:`decorate` use
              + call ``advice.__init__(self)``

        :param before: code to run before the excecution of the
                       decorated function
        :type before: `function` `instancemethod` or `list` of
                      `function` or `instancemethod`
        :param around: code to run around the excecution of the
                       decorated function
        :type arount: `function` `instancemethod` or `list` of
                      `function` or `instancemethod`
        :param after: code to run after the excecution of the
                      decorated function
        :type after: `function` or `instancemethod` or `list` of
                     `function` or `instancemethod`
        :param decorated: function or method to decorate
        :type decorated: `function` or `instancemethod`
        :raise ValueError: if none of the :data:`before`,
                           :data:`around` or :data:`after` is passed
                           or if a positional argument is passed.

        """
        if faulty_positional is not None:
            raise ValueError(u'Only keyword arguments authorized.')

        if before is None and around is None and after is None:
            raise ValueError(u'Missing at least one advice function')

        self.before = []
        self.around = []
        self.after = []

        self._register_advice('before', before)
        self._register_advice('around', around)
        self._register_advice('after', after)

        # Pass decorated function as keyword argument
        EoleDecorator.__init__(self, decorated=decorated)

    def _register_advice(self, name, value):
        """Register advices.

        If a list is passed as :data:`value`, then recursively
        register each item for the current modifier.

        :param name: name of the modifier
        :type name: `str`
        :param value: code to register for the modifier
        :type value: `function` or `instancemethod` or `list` of
                     `function` or `instancemethod`

        """
        if value is not None:
            if isinstance(value, list):
                for code in value:
                    self._register_advice(name, code)
            elif not callable(value):
                msg = u"'{0}' advice is not callable: {1}"
                raise ValueError(msg.format(name, value))
            else:
                getattr(self, name).append(value)

    def _call_around(self, functions=None, *args, **kwargs):
        """Recursively call around modifiers.

        The calls are nested in reversed order, the last added
        :attr:`around` modifier is run first.

        If an around modifier does not call the function it receive at
        its first argument, the recursion is stopped.

        :param function: function to run
        :type function: `list` of `function`
        :return: return value of the last added :attr:`around`

        """
        if len(functions) == 1:
            # This is the decorated function
            return functions[0](*args, **kwargs)
        elif len(functions) > 1:
            around_next = functions.pop()
            return around_next(self._call_around, functions, *args,
                               **kwargs)
        else:
            raise TypeError(u'No around function found')

    def decorate(self, *args, **kwargs):
        """Advice the decorated function.

        Run the 3 modifiers lists:

          - :attr:`before` modfiers are run in sequential reversed
            order

          - :attr:`around` modifiers are run in nested reversed order

          - :attr:`after` modifiers are run in normal order

          - the returned value is the one of the last called
            :attr:`around` function.

        If no :attr:`around` modifier is provided, run the decorated
        function.

        The stacklevel of the adviced function in :attr:`before`, and
        :attr:`after` modifiers is `4`.

        The stacklevel of the adviced function in :attr:`around` is
        5 + 2 times (the number :attr:`around` modifiers minus the
        number of remaining modifiers):

          `5 + 2 * ( len(self.around) - len(remaining_modifiers) )`

        :param args: arguments for the decorated function or method
        :type args: `tuple`
        :param kwargs: keyword arguments for the decorated function or
                       method
        :type kwargs: `dict`
        :return: the returned value of the decorated function or last
                 called :attr:`around`

        """
        ret = None

        for function in reversed(self.before):
            function(self.decorated, *args, **kwargs)

        if len(self.around) > 0:
            # Be sur to copy self.around since it's poped by _call_around
            ret = self._call_around(self.around[:], self.decorated,
                                    *args, **kwargs)
        else:
            ret = self.decorated(*args, **kwargs)

        for function in self.after:
            function(ret, self.decorated, *args, **kwargs)

        return ret


class deprecated(advice):
    """Decorator to mark a functions as deprecated.

    It will result in a warning being emmitted when the function is
    used with information on the caller of the deprecated function.

    An additional message can be passed as parameter to override the
    default one.

    A rewrite dead-line can be passed as ``(year, month, day)`` arguments
    to the decorator to raise a :exc:`GruikIsDead` exception when the
    time is over.

    Example::

      from pyeole.decorator import deprecated

      @deprecated("use new function new_function()")
      def some_old_function(x,y):
          new_function(x,y)

      class SomeClass(object):
          @deprecated(message="use new method .new_method()")
          def some_old_method(self, x,y):
              self.new_method(x,y)

      @deprecated(message="use new function new_function() before it raises",
                  year=2014, month=2, day=5)
      def some_old_function(x,y):
          new_function(x,y)

    """
    def __init__(self, message=None, year=None, month=None, day=None,
                 deadline_raise=False):
        """Initialize the deprecated advice decorator.

        It will advice the decorated function with a
        :attr:`advice.before` function which inform who called the
        deprecated function.

        If :func:`datetime.date.today()` is after the defined date
        passed as argument a log.error is emit instead of silent warning.

        If :data:`deadline_raise` is ``True``, an :exc:`GruikIsDead`
        exception is raised to force the developper to rewrite the
        function (or increase the dead-line).

        :param message: additional message to display
        :type message: `str`
        :param year: year of the dead-line
        :type year: `int`
        :param month: month of the dead-line
        :type month: `int`
        :param day: day of the month of the dead-line
        :type day: `int`
        :param deadline_raise: require a raise after deadline
        :type deadline_raise: `bool`

        """
        self.message = u"Deprecated function call '{callee}' by '{caller}'"

        # End of “must rewrite”
        self.deadline = None
        self.deadline_raise = deadline_raise

        if not hasattr(self, 'exception'):
            self.exception = FixMeError

        # Caller function
        self.caller = None
        self.formated_caller = None
        # Get a logger for caller name space
        self.log = None

        try:
            self.deadline = date(year=year, month=month, day=day)
        except TypeError:
            # User pass garbage
            pass

        if message is not None and callable(message):
            # No message provided, message is the decorated function
            # pass it as keyword parameter to advice
            advice.__init__(self, decorated=message,
                            before=self.deprecate)
        elif (message is not None
              and not (isinstance(message, str)
                       or isinstance(message, unicode))):
            msg = u'Wrong use of positional arguments, read the doc'
            raise ValueError(msg)
        else:
            if message is not None:
                self.message = u'{0}: {1}'.format(self.message, message)

            advice.__init__(self, before=self.deprecate)

    def deprecate(self, decorated, *args, **kwargs):
        """Display a deprecation warning with caller information.

        :param decorated: deprecated function or method
        :type decorated: `function` or `instancemethod`
        :param args: positional arguments for the deprecated function
                     or method
        :type args: `tuple`
        :param kwargs: keyword arguments for the deprecated function or
                       method
        :type kwargs: `dict`

        """
        # The caller of deprecated function is 4 levels up in the stack
        # caller->__call__->decorate->->before->deprecate
        stacklevel = 4
        caller = get_caller_infos(stacklevel=stacklevel)
        # Get or initialize the caller logger
        # Take care to reinitialize the logger name depending on the caller
        if self.log is None or self.caller != self.log.name:
            # All caller informations
            caller_format = u'{modulename}.{classname}.{codename}#L{lineno}'
            # Name of the caller
            self.caller = format_caller(caller)
            # Name to display in message
            self.formated_caller = format_caller(caller,
                                                 format_string=caller_format)
            self.log = logging.getLogger(self.caller)
            # Avoid message about missing log handler
            self.log.addHandler(logging.NullHandler())

        now = date.today()

        if self.log.getEffectiveLevel() == logging.DEBUG:
            # Full caller name, **caller my be used by user supplied message
            message = self.message.format(callee=decorated.__name__,
                                          caller=self.formated_caller,
                                          **caller)
        else:
            # Simple caller name, **caller my be used by user supplied message
            message = self.message.format(callee=decorated.__name__,
                                          caller=self.caller,
                                          **caller)
        if self.deadline is not None and self.deadline - now <= timedelta(0):
            if self.deadline_raise:
                raise self.exception(message)
            else:
                self.log.error(message)
        else:
            warnings.warn(message, category=DeprecationWarning,
                          stacklevel=stacklevel)


class fixme(deprecated):
    """Decorator to mark a function for rewrite.

    It will result in a warning being emmited when the function is
    used with information on the caller of to-be-fixed function.

    A rewrite dead-line is passed as ``(year, month, day)`` arguments
    to the decorator to raise a :exc:`FixMeError` exception when the
    time is over.

    Example::

        from pyeole.decorator import fixme

        @fixme(year=2014, month=2, day=31)
        def to_be_rewritten(x,y):
            some_bad_code()

        @fixme(year=2013, message="'{callee}' need to be optimized")
        def to_be_optimized():
            run_non_optimized_code

    """
    def __init__(self, decorated=None, year=2013, month=12, day=1,
                 message=None):
        """Initialize the fixme decorator.

        Accepts only keyword parameters, the :data:`decorated`
        argument is here to detect if positional argument was passed.

        It will advice the decorated function with a
        :attr:`advice.before` function which inform who called the
        to-be-fixed function.

        If :func:`datetime.date.today()` is after the defined date
        passed as argument a :exc:`FixMeError` exception is raised to
        force the developper to rewrite the function (or increase the
        dead-line).

        :param year: year of the dead-line
        :type year: `int`
        :param month: month of the dead-line
        :type month: `int`
        :param day: day of the month of the dead-line
        :type day: `int`
        :param message: message to display, can use ``{caller}`` for
                        caller name and ``{callee}`` for the
                        decorated function
        :type message: `str`

        """
        self.message = u"FIXME detected in '{callee}' called by '{caller}'"

        deprecated.__init__(self, year=year, month=month, day=day,
                            message=message, decorated=decorated)


class gruik(deprecated):
    """Decorator to mark a function for rewrite.

    It will result in a :func:`log.warn` being emmited when the
    function is used with information on the caller of the gruik
    function.

    A rewrite dead-line is passed as ``(year, month, day)`` arguments
    to the decorator to raise a :exc:`GruikIsDead` exception when the
    time is over.

    Example::

        from pyeole.decorator import gruik

        @gruik(year=2014, month=2, day=31)
        def to_be_rewritten(x,y):
            some_bad_code()

        msg = "'{callee}' must make argument sanity checks"
        @gruik(year=2013, message=msg)
        def dangerous_function(cmd):
            os.system(cmd)

    """
    def __init__(self, decorated=None, year=2013, month=12, day=1,
                 message=None):
        """Initialize the gruik decorator.

        Accepts only keyword parameters, the :data:`decorated`
        argument is here to detect if positional argument was passed.

        It will advice the decorated function with a
        :attr:`advice.before` function which inform who called the
        gruik function.

        If :func:`datetime.date.today()` is after the defined date
        passed as argument a :exc:`GruikIsDead` exception is raised to
        force the developper to rewrite the function (or increase the
        dead-line).

        :param year: year of the dead-line
        :type year: `int`
        :param month: month of the dead-line
        :type month: `int`
        :param day: day of the month of the dead-line
        :type day: `int`
        :param message: message to display, can use ``{caller}`` for
                        caller name and ``{callee}`` for the
                        decorated function
        :type message: `str`

        """
        msg = u"Gruik code detected in '{callee}' called by '{caller}'"
        self.message = msg

        self.exception = GruikIsDead

        deprecated.__init__(self, year=year, month=month, day=day,
                            message=message, decorated=decorated)


class FixMeError(Exception):
    """Some code is too badly written and need a rewrite.

    Inherit from :exc:`Exception` instead of :exc:`StandardError` to
    limit catching by user code.
    """
    pass


class GruikIsDead(Exception):
    """Some code is too badly written and need a rewrite.

    Inherit from :exc:`Exception` instead of :exc:`StandardError` to
    limit catching by user code.
    """
    pass
