package org.multiverse.api;

import java.util.Random;

import static java.util.concurrent.locks.LockSupport.parkNanos;

/**
 * A {@link BackoffPolicy} that does an 'exponential' backoff. So each next attempt, the calculated delay is increased
 * and randomized (so the next value can be smaller than the previous, but overall they will increase).
 *
 * @author Peter Veentjer.
 */
@SuppressWarnings({"CallToThreadYield"})
public final class DefaultBackoffPolicy implements BackoffPolicy {

    public final static BackoffPolicy MAX_100_MS = new DefaultBackoffPolicy();

    private final long minDelayNs;
    private final long[] slotTimes;

    /**
     * Creates an ExponentialBackoffPolicy with 100 nanoseconds as minimal delay and 100 milliseconds as maximum
     * delay.
     */
    public DefaultBackoffPolicy() {
        this(1000);
    }

    /**
     * Creates an ExponentialBackoffPolicy with given maximum delay.
     *
     * @param minDelayNs the minimum delay in nanoseconds to wait. If a negative or zero value provided, it will be
     *                   interpreted that no external minimal value is needed.
     * @throws NullPointerException if unit is null.
     */
    public DefaultBackoffPolicy(long minDelayNs) {
        Random random = new Random();
        slotTimes = new long[1000];
        this.minDelayNs = minDelayNs;

        double a = 100;
        double b = -4963;
        for (int k = 0; k < slotTimes.length; k++) {
            slotTimes[k] = Math.round(random.nextDouble() * f(k, a, b));
        }
    }

    private int f(int x, double a, double b) {
        int result = (int) Math.round(a * x * x + b * x);
        return result < 0 ? 0 : result;
    }

    @Override
    public void delay(int attempt) throws InterruptedException {
        delayUninterruptible(attempt);
    }

    @Override
    public void delayUninterruptible(int attempt) {
        long delayNs = calcDelayNs(attempt);

        if (delayNs >= minDelayNs) {
            parkNanos(delayNs);
        } else if (attempt % 20 == 0) {
            Thread.yield();
        }
    }

    protected long calcDelayNs(int attempt) {
        int slotIndex = attempt >= slotTimes.length ? slotTimes.length - 1 : attempt;
        return slotTimes[slotIndex];
    }
}

