import math
import functools
import time


def retry(tries, delay = 3, backoff = 2):
    """ Retries a function or method until it returns ``True`` or runs out of retries.

    Retries are performed at a specific delay with an additional back off term. The retries go
    as

    .. math::

        t = delay * backoff^(nTry)

    where :math:`nTry` is the trial that we are on. Note that 2 retries corresponds to three function
    calls - the original call, and then up to two retries if the calls fail.

    Original decorator from the `Python wiki <https://wiki.python.org/moin/PythonDecoratorLibrary#Retry>`__,
    and using some additional improvements `here <https://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/>`__,
    and some ideas `here <https://www.calazan.com/retry-decorator-for-python-3/>`__.

    Args:
        tries (int): Number of times to retry.
        delay (float): Delay in seconds for the initial retry. Default: 3.
        backoff (float): Amount to multiply the delay by between each retry. See the formula above.
    Returns:
        bool: True if the function succeeded.
    """
    # Argument validation
    if backoff <= 1:
        raise ValueError("Backoff must be greater than 1.")
    tries = math.floor(tries)
    if tries < 0:
        raise ValueError("Tries must be 0 or greater.")
    if delay <= 0:
        raise ValueError("Delay must be greater than 0.")

    def deco_retry(f):
        @functools.wraps(f)
        def f_retry(*args, **kwargs):
            # Make mutable
            mtries, mdelay = tries, delay

            # First attempt at calling the function
            rv = f(*args, **kwargs)
            while mtries > 0:
                # If we ever get a return value of `True`, we are done.
                if rv is True:
                    return True

                # Setup for the next attempt and wait before the next attempt
                mtries -= 1
                time.sleep(mdelay)
                mdelay *= backoff

                # Try again
                rv = f(*args, **kwargs)

            # Ran out of tries. Return failure.
            return False

        # true decorator -> decorated function
        return f_retry
    # @retry(arg[, ...]) -> true decorator
    return deco_retry