package de.duehl.basics.retry;

/*
 * Copyright 2017 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import de.duehl.basics.logging.Logger;
import de.duehl.basics.random.Randomizer;
import de.duehl.basics.system.ExceptionHelper;
import de.duehl.basics.system.SystemTools;

/**
 * Diese Klasse versucht eine Weile lang wiederholt die angegebene Aktion auszuführen.
 *
 * @version 1.01     2017-06-22
 * @author Christian Dühl
 */

public class Retry {

    /** Anzahl der Versuche die Backup-Suiten zu lesen, wenn das Lesen fehlschlägt. */
    private static final int INITIAL_MAXIMUM_NUMBER_OF_TRIES = 10;

    /** Diese Zeitspanne wird geschlafen, wenn es einen Fehler beim Ausführen der Aktion gab. */
    private static final long INITIAL_MILLISECONDS_TO_SLEEP_BY_ERRORS = 250;

    /** Objekt das man mehrfach versuchen lassen kann, etwas auszuführen. */
    private final Retryable retryable;

    /** Anzahl der Versuche die Backup-Suiten zu lesen, wenn das Lesen fehlschlägt. */
    private final int maximumNumberOfTries;

    /** Diese Zeitspanne wird geschlafen, wenn es einen Fehler beim Ausführen der Aktion gab. */
    private final long millisecondsToSleepByErrors;

    /** Der Logger (kann null sein). */
    private final Logger logger;

    /** Zähler für die Anzahl der Versuche. */
    private int tryCount;

    /** Gibt an, ob die Aufgabe erfolgreich ausgeführt wurde. */
    private boolean sucessfullyDone;

    /** Gibt an, ob zu viele Versuche gebraucht wurden, um die Aufgabe erfolgreich zu erledigen. */
    private boolean allTriesFailed;

    /**
     * Gibt an, ob die übergeben Zeit, die geschlafen wird, variiert werden soll, um immer wieder
     * gleiche Versuche zur selben Zeit von verschiedenen Prozessen zu vermeiden.
     */
    private boolean randomizeSleepTime;

    private final Randomizer randomizer;

    /**
     * Konstruktor.
     *
     * @param retryable
     *            Objekt das man mehrfach versuchen lassen kann, etwas auszuführen.
     */
    public Retry(Retryable retryable) {
        this(retryable, INITIAL_MAXIMUM_NUMBER_OF_TRIES, INITIAL_MILLISECONDS_TO_SLEEP_BY_ERRORS,
                null);
    }

    /**
     * Konstruktor.
     *
     * @param retryable
     *            Objekt das man mehrfach versuchen lassen kann, etwas auszuführen.
     * @param logger
     *            Der Logger.
     */
    public Retry(Retryable retryable, Logger logger) {
        this(retryable, INITIAL_MAXIMUM_NUMBER_OF_TRIES, INITIAL_MILLISECONDS_TO_SLEEP_BY_ERRORS,
                logger);
    }

    /**
     * Konstruktor.
     *
     * @param retryable
     *            Objekt das man mehrfach versuchen lassen kann, etwas auszuführen.
     * @param maximumNumberOfTries
     *            Anzahl der Versuche die Backup-Suiten zu lesen, wenn das Lesen fehlschlägt.
     * @param millisecondsToSleepByErrors
     *            Diese Zeitspanne wird geschlafen, wenn es einen Fehler beim Ausführen der Aktion
     *            gab.
     */
    public Retry(Retryable retryable, int maximumNumberOfTries, long millisecondsToSleepByErrors) {
        this(retryable, maximumNumberOfTries, millisecondsToSleepByErrors, null);
    }

    /**
     * Konstruktor.
     *
     * @param retryable
     *            Objekt das man mehrfach versuchen lassen kann, etwas auszuführen.
     * @param maximumNumberOfTries
     *            Anzahl der Versuche die Backup-Suiten zu lesen, wenn das Lesen fehlschlägt.
     * @param millisecondsToSleepByErrors
     *            Diese Zeitspanne wird geschlafen, wenn es einen Fehler beim Ausführen der Aktion
     *            gab.
     * @param logger
     *            Der Logger.
     */
    public Retry(Retryable retryable, int maximumNumberOfTries, long millisecondsToSleepByErrors,
            Logger logger) {
        this.retryable = retryable;
        this.maximumNumberOfTries = maximumNumberOfTries;
        this.millisecondsToSleepByErrors = millisecondsToSleepByErrors;
        this.logger = logger;

        tryCount = 0;
        sucessfullyDone = false;
        allTriesFailed = false;
        randomizeSleepTime = false;
        randomizer = new Randomizer();
    }

    /**
     * Variiert die übergeben Zeit, die geschlafen wird, um immer wieder gleiche Versuche zur
     * selben Zeit von verschiedenen Prozessen zu vermeiden.
     */
    public void randomizeSleepTime() {
        randomizeSleepTime = true;
    }

    /** Führe so lange die übergebene Aufgabe aus, bis sie Erfolg hatte. */
    public void tryAndTry() {
        checkTryCountStart();
        loop();
    }

    private void checkTryCountStart() {
        if (tryCount != 0) {
            String message = "Diese Klasse darf nicht mehrfach verwendet werden!";
            log(message);
            throw new RuntimeException(message);
        }
    }

    private void loop() {
        while (!sucessfullyDone && !allTriesFailed) {
            ++tryCount;
            log("tryCount = '" + tryCount + "'");
            checkTryCountInLoop();
            if (!allTriesFailed) {
                tryRetryable();
            }
        }
    }

    private void checkTryCountInLoop() {
        if (tryCount > maximumNumberOfTries) {
            allTriesFailed = true;
            log("Zu viele Versuche!");
        }
    }

    private void tryRetryable() {
        try {
            retryable.tryIt();
            sucessfullyDone = true;
            log("Success!");
        }
        catch (Exception exception) {
            String message = "Beim Versuch, die Aufgabe zu erledigen trat eine Ausnahme auf: "
                    + ExceptionHelper.getExceptionNameAndMessage(exception);
            log(message);
            if (tryCount < maximumNumberOfTries) { // Verhindert das letzte Schlafen wenn alles
                sleep();                           // schief geht. Geschlafen wird nur, wenn
            }                                      // danach noch Versuche folgen.
        }
    }

    private void sleep() {
        SystemTools.sleep(determineSleepTime());
    }

    private long determineSleepTime() {
        if (randomizeSleepTime) {
            return calculateRandomizedSleeptime();
        }
        else {
            return millisecondsToSleepByErrors;
        }
    }

    private long calculateRandomizedSleeptime() {
        return randomizeFrom90To110Percent(millisecondsToSleepByErrors);
    }

    long randomizeFrom90To110Percent(long number) {
        return randomizer.randomizeFrom90To110Percent(number);
    }

    /** Gibt an, ob alle Versuche fehlschlugen. */
    public final boolean haveAllTriesFailed() {
        return allTriesFailed;
    }

    /** Gibt an, ob die Aufgabe erfolgreich ausgeführt wurde. */
    public final boolean isSucessfullyDone() {
        return sucessfullyDone;
    }

    /** Loggt die übergebene Nachricht. */
    private void log(String message) {
        if (null != logger) {
            logger.log(message, 1);
        }
    }

}
