"""This module provides pre/post hooks for the sync system.

Attributes
----------
_hooks : dict
    List the list of functions to call for each type of hook (defined in ``_allowed_when``)
_allowed_when : set
    List the values allowed to pass for the ``when`` argument of the ``_sync_hook`` decorator.
    Actually {'pre', 'post'}

Example
-------
>>> from django.conf import settings

>>> @pre_sync_hook(settings.INSTANCE_IS_MASTER)
... def func_to_run_on_master_before_the_sync():
...     pass

>>> @pre_sync_hook(settings.INSTANCE_IS_SLAVE)
... def func_to_run_on_slave_before_the_sync():
...     pass

>>> @post_sync_hook(settings.INSTANCE_IS_MASTER)
... def func_to_run_on_master_after_the_sync():
...     pass

>>> @post_sync_hook(settings.INSTANCE_IS_SLAVE)
... def func_to_run_on_slave_after_the_sync():
...     pass

>>> @pre_sync_hook
... def func_to_run_on_master_and_slave_before_the_sync():
...     pass

>>> @post_sync_hook
... def func_to_run_on_master_and_slave_after_the_sync():
...     pass

>>> # Run the hook on a server before the sync:
>>> run_pre_sync_hooks()

>>> # Run the hook on a server after the sync:
>>> run_post_sync_hooks()

"""

import functools
from inspect import signature


_hooks = {}
_allowed_when = {'pre', 'post'}

def _sync_hook(func=None, activated=True, when=None):
    """Base decorator to save a function to be run during the sync.

    This function should not be used directly. Use for example ``pre_sync_hook`` and
    ``post_sync_hook``.

    Parameters
    ----------
    func : function
        The function to register. Must not take arguments, or only ones with default values.
    activated : boolean, default ``True``.
        If we want this hook to be executed.. This argument is here to accept something like
        ``settings.INSTANCE_IS_MASTER`` (or ``settings.INSTANCE_IS_SLAVE``) to only execute
        this hook on a master (or a slave).
        The default value allow to execute this hook everywhere.
    when : string (one from ``_allowed_when``)
        When this hook should be executed.

    Returns
    -------
    func
        The given, unmodified function.

    """

    # When the decorator is called with arguments, the first argument we got is not the function
    # but the first passed one, so it's ``activated``. We set the correct values.
    if isinstance(func, bool):
        func, activated = None, func

    # Validate arguments.
    if not isinstance(activated, bool):
        raise ValueError('The `activated` argument must be a boolean')
    if when not in _allowed_when:
        raise ValueError('Then `when` argument should be one of %s' % _allowed_when)

    if func is None:
        # We didn't have yet the function to decorate because of the arguments, so we
        # return a new decorator function with arguments that will be applied
        # to decorate the function.
        return functools.partial(_sync_hook, activated=activated, when=when)
    else:
        if isinstance(func, (classmethod, staticmethod)):
            raise ValueError('This decorator works only on simple functions')

    # Ensure that the function has no arguments, or only arguments with default values.
    # This is needed because the hooks are called one by one, without arguments.
    if not all([p.default is not p.empty for p in signature(func).parameters.values()]):
        raise ValueError('The decorated function should not take arguments without default value')

    # Only add the hook to the ones to execute if asked.
    if activated:
        _hooks.setdefault(when, []).append(func)

    # Return the given function, unmodified
    return func

# Decorator to use to have a function executed before the sync.
pre_sync_hook = functools.partial(_sync_hook, when='pre')
# Decorator to use to have a function executed after the sync.
post_sync_hook = functools.partial(_sync_hook, when='post')

def _run_hooks(when=None):
    # Base function to execute all the hooks saved to be run with the given ``when``.
    for hook in _hooks.get(when, []):
        hook()

# Function to use to execute all hooks registered to be run before the sync.
run_pre_sync_hooks = functools.partial(_run_hooks, when='pre')
# Function to use to execute all hooks registered to be run after the sync.
run_post_sync_hooks = functools.partial(_run_hooks, when='post')
