package ru.yandex.webmaster.common.util;

import java.util.Optional;

import org.joda.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.yandex.webmaster.common.util.functional.ThrowingRunnable;
import ru.yandex.webmaster.common.util.functional.ThrowingSupplier;

/**
 * Created by aleksart on 26.07.17.
 */
public final class RetryUtils {
    private static final Logger log = LoggerFactory.getLogger(RetryUtils.class);
    private RetryUtils(){};

    public static boolean tryExecute(RetryPolicy policy, ThrowingRunnable<Exception> callback) throws InterruptedException {
        int failedAttempts = 0;
        while (!Thread.interrupted()) {
            try {
                callback.run();
                return true;
            } catch (Exception e) {
                failedAttempts++;
                Optional<Duration> retryAfter = policy.retryAfter(failedAttempts);
                if (retryAfter.isPresent()) {
                    log.warn("Execution failed, retry after " + retryAfter.get(), e);
                    Thread.sleep(retryAfter.get().getMillis());
                } else {
                    log.error("Execution totally failed", e);
                    return false;
                }
            }
        }
        throw new InterruptedException();
    }

    public static <E extends Exception> void execute(RetryPolicy policy, ThrowingRunnable<E> callback) throws InterruptedException, E {
        query(policy, () -> {
            callback.run();
            return (Void) null;
        });
    }

    public static <T, E extends Exception> T query(RetryPolicy policy, ThrowingSupplier<T, E> producer) throws InterruptedException, E {
        int failedAttempts = 0;
        while (!Thread.interrupted()) {
            try {
                return producer.get();
            } catch (Exception e) {
                failedAttempts++;
                Optional<Duration> retryAfter = policy.retryAfter(failedAttempts);
                if (retryAfter.isPresent()) {
                    log.warn("Execution failed, retry after " + retryAfter.get(), e);
                    Thread.sleep(retryAfter.get().getMillis());
                } else {
                    throw e;
                }
            }
        }
        throw new InterruptedException();
    }

    public interface RetryPolicy {
        Optional<Duration> retryAfter(int failedAttempts);
    }

    public static RetryPolicy instantRetry(int attempts) {
        return failedAttempts -> {
            if (failedAttempts >= attempts) {
                return Optional.empty();
            } else {
                return Optional.of(Duration.ZERO);
            }
        };
    }

    public static RetryPolicy linearBackoff(int attempts, Duration resultingDuration) {
        int maxAttempts = Math.max(2, attempts);
        Duration step = resultingDuration.multipliedBy(2).dividedBy((maxAttempts - 1) * maxAttempts);
        return failedAttempts -> {
            if (failedAttempts >= maxAttempts) {
                return Optional.empty();
            } else {
                return Optional.of(step.multipliedBy(failedAttempts));
            }
        };
    }

    public static RetryPolicy expBackoff(int attempts, Duration initialWait) {
        return failedAttempts -> {
            if (failedAttempts > attempts) {
                return Optional.empty();
            }
            Duration wait;
            if (failedAttempts > 1) {
                int waitMulti = 1 << (failedAttempts - 1);
                wait = initialWait.multipliedBy(waitMulti);
            } else {
                wait = initialWait;
            }
            return Optional.of(wait);
        };
    }


    private static final RetryPolicy NEVER = failedAttempts -> Optional.empty();
    public static RetryPolicy never() {
        return NEVER;
    }

}


