package de.duehl.swing.ui.update;

/*
 * Copyright 2025 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 java.util.Timer;
import java.util.TimerTask;

import de.duehl.basics.datetime.DateAndTime;
import de.duehl.basics.datetime.time.watch.StopWatch;
import de.duehl.basics.debug.DebugHelper;
import de.duehl.basics.text.NumberString;
import de.duehl.swing.logic.Quitter;
import de.duehl.swing.ui.update.informer.NoUpdateAfterDayChangeInformer;
import de.duehl.swing.ui.update.informer.NoUpdateIntervalInSecondsChangeInformer;
import de.duehl.swing.ui.update.informer.UpdateAfterDayChangeInformer;
import de.duehl.swing.ui.update.informer.UpdateIntervalInSecondsChangeInformer;

/**
 * Diese Klasse stellt die Logik zur regelmäßigen Ausführung eines Tasks dar.
 *
 * @version 1.01     2025-11-13
 * @author Christian Dühl
 */

public class UpdateLogic implements Quitter {

    public static final int MINIMAL_UPDATE_INTERVAL_IN_SECONDS = 5;       //  5 Sekunden
    public static final int MAXIMAL_UPDATE_INTERVAL_IN_SECONDS = 30 * 60; // 30 Minuten
    public static final int UPDATE_INTERVAL_IN_SECONDS = 10;

    private static final boolean DEBUG = false;

    /** Die regelmäßig auszuführende Aufgabe. */
    private final Runnable job;

    /** Beschreibung des Jobs für den Wartebildschirm. */
    private final String jobDescritpion;

    /** Grafische Oberfläche zur regelmäßigen Ausführung des Tasks. */
    private final UpdateGui gui;

    /** Minimaler Abstand zwischen den Ausführungen des Tasks in Sekunden. */
    private final int minimalUpdateIntervalInSeconds;

    /** Maximaler Abstand zwischen den Ausführungen des Tasks in Sekunden. */
    private final int maximalUpdateIntervalInSeconds;

    /** Der Timer, der zum warten auf das nächste Update verwendet wird. */
    private Timer timer;

    /** Der Abstand zwischen den Ausführungen des Tasks in Sekunden. */
    private int updateIntervalInSeconds;

    /** Die Anzahl Ausführungen des Tasks. */
    private int updateCount;

    /**
     * Wird darüber informiert, wenn ein Update das erste Mal nach dem Tageswechsel erfolgt. Dafür
     * muss am Tag zuvor bereits mindestens ein Update gelaufen sein.
     */
    private UpdateAfterDayChangeInformer updateAfterDayChangeInformer;

    /**
     * Wird darüber informiert, wenn der Abstand zwischen den Ausführungen des Tasks in Sekunden
     * geändert wurde.
     */
    private UpdateIntervalInSecondsChangeInformer updateIntervalInSecondsChangeInformer;

    /** Gibt an, ob bereits einmal ein Update lief. */
    private boolean wasUpdated;

    /** Zeitpunkt des letzten Updates. */
    private DateAndTime lastUpdateDateAndTime;

    /**
     * Gibt an, ob das Update-Intervall automatisch verlängert wird, wenn der letzte Arbeitsschritt
     * zu lange dauerte.
     */
    private boolean changeUpdateIntervalIfUpdateTookTooLong;

//    /**
//     * Konstruktor.
//     *
//     * @param job
//     *            Die regelmäßig auszuführende Aufgabe.
//     * @param jobDescritpion
//     *            Beschreibung des Jobs für den Wartebildschirm.
//     * @param gui
//     *            Grafische Oberfläche zur regelmäßigen Ausführung des Tasks.
//     */
//    public UpdateLogic(Runnable job, String jobDescritpion, UpdateGui gui) {
//        this(job, jobDescritpion, gui, MINIMAL_UPDATE_INTREVAL_IN_SECONDS,
//                MAXIMAL_UPDATE_INTREVAL_IN_SECONDS);
//    }

    /**
     * Konstruktor.
     *
     * @param job
     *            Die regelmäßig auszuführende Aufgabe.
     * @param jobDescritpion
     *            Beschreibung des Jobs für den Wartebildschirm.
     * @param gui
     *            Grafische Oberfläche zur regelmäßigen Ausführung des Tasks.
     * @param updateIntervalInSeconds
     *            Abstand zwischen den Ausführungen des Tasks in Sekunden.
     */
    public UpdateLogic(Runnable job, String jobDescritpion, UpdateGui gui,
            int updateIntervalInSeconds) {
        this(job, jobDescritpion, gui, updateIntervalInSeconds, MINIMAL_UPDATE_INTERVAL_IN_SECONDS,
                MAXIMAL_UPDATE_INTERVAL_IN_SECONDS);
    }

//    /**
//     * Konstruktor.
//     *
//     * @param job
//     *            Die regelmäßig auszuführende Aufgabe.
//     * @param jobDescritpion
//     *            Beschreibung des Jobs für den Wartebildschirm.
//     * @param gui
//     *            Grafische Oberfläche zur regelmäßigen Ausführung des Tasks.
//     * @param minimalUpdateIntervalInSeconds
//     *            Minimaler Abstand zwischen den Ausführungen des Tasks in Sekunden.
//     * @param maximalUpdateIntervalInSeconds
//     *            Maximaler Abstand zwischen den Ausführungen des Tasks in Sekunden.
//     */
//    public UpdateLogic(Runnable job, String jobDescritpion, UpdateGui gui,
//            int minimalUpdateIntervalInSeconds, int maximalUpdateIntervalInSeconds) {
//        this(job, jobDescritpion, gui, UPDATE_INTERVAL_IN_SECONDS, minimalUpdateIntervalInSeconds,
//                maximalUpdateIntervalInSeconds);
//    }

    /**
     * Konstruktor.
     *
     * @param job
     *            Die regelmäßig auszuführende Aufgabe.
     * @param jobDescritpion
     *            Beschreibung des Jobs für den Wartebildschirm.
     * @param gui
     *            Grafische Oberfläche zur regelmäßigen Ausführung des Tasks.
     * @param updateIntervalInSeconds
     *            Abstand zwischen den Ausführungen des Tasks in Sekunden.
     * @param minimalUpdateIntervalInSeconds
     *            Minimaler Abstand zwischen den Ausführungen des Tasks in Sekunden.
     * @param maximalUpdateIntervalInSeconds
     *            Maximaler Abstand zwischen den Ausführungen des Tasks in Sekunden.
     */
    public UpdateLogic(Runnable job, String jobDescritpion, UpdateGui gui,
            int updateIntervalInSeconds, int minimalUpdateIntervalInSeconds,
            int maximalUpdateIntervalInSeconds) {
        this.job = job;
        this.jobDescritpion = jobDescritpion;
        this.gui = gui;
        this.updateIntervalInSeconds = updateIntervalInSeconds;
        this.minimalUpdateIntervalInSeconds = minimalUpdateIntervalInSeconds;
        this.maximalUpdateIntervalInSeconds = maximalUpdateIntervalInSeconds;

        updateAfterDayChangeInformer = new NoUpdateAfterDayChangeInformer();
        updateIntervalInSecondsChangeInformer = new NoUpdateIntervalInSecondsChangeInformer();
        changeUpdateIntervalIfUpdateTookTooLong = true;

        gui.setlogic(this);

        updateCount = 0;
    }

    /**
     * Setzt das Objekt, welches darüber informiert wird, wenn ein Update das erste Mal nach dem
     * Tageswechsel erfolgt. Dafür muss am Tag zuvor bereits mindestens ein Update gelaufen sein.
     */
    public void setUpdateAfterDayChangeInformer(
            UpdateAfterDayChangeInformer updateAfterDayChangeInformer) {
        this.updateAfterDayChangeInformer = updateAfterDayChangeInformer;
    }

    /**
     * Setzt das Objekt, welches darüber informiert wird, wenn der Abstand zwischen den
     * Ausführungen des Tasks in Sekunden geändert wurde.
     */
    public void setUpdateIntervalInSecondsChangeInformer(
            UpdateIntervalInSecondsChangeInformer updateIntervalInSecondsChangeInformer) {
        this.updateIntervalInSecondsChangeInformer = updateIntervalInSecondsChangeInformer;
    }

    /**
     * Legt fest, dass das Update-Intervall NICHT automatisch verlängert wird, wenn der letzte
     * Arbeitsschritt zu lange dauerte.
     *
     * Normalerweise sollte man diese Möglichkeit nicht abschalten.
     */
    public void doNotChangeUpdateIntervalIfUpdateTookTooLong() {
        changeUpdateIntervalIfUpdateTookTooLong = false;
    }

    /**
     * Prüft, ob der übergebene Abstand zwischen den Ausführungen des Tasks in Sekunden im
     * zulässigen Bereich liegt, setzt ihn aber noch nicht.
     */
    String checkUpdateIntervalInSecondsIsInRange(int updateIntervalInSeconds) {
        if (updateIntervalInSeconds < minimalUpdateIntervalInSeconds
                || updateIntervalInSeconds > maximalUpdateIntervalInSeconds) {
            int maxMinutes = maximalUpdateIntervalInSeconds / 60;
            return "Das Aktualisierungsintervall muss statt "
                    + NumberString.taupu(updateIntervalInSeconds) + " zwischen "
                    + NumberString.taupu(minimalUpdateIntervalInSeconds) + " Sekunden und "
                    + NumberString.taupu(maxMinutes) + " Minuten liegen!";
        }
        else {
            return "";
        }
    }

    /**
     * Wird aufgerufen, um den Abstand zwischen den Ausführungen des Tasks in Sekunden zu setzen,
     * ohne den Task laufen zu lassen.
     *
     * Falls der Wert nicht zu minimal und maximal erlaubten Wert passt, wird der Aufruf ignoriert.
     *
     * Etwa nachdem dieser Wert aus den Optionen eines Programms geladen wurde.
     */
    public void setUpdateIntervalInSecondsWithoutRun(int updateIntervalInSeconds) {
        String errorMessage = checkUpdateIntervalInSecondsIsInRange(updateIntervalInSeconds);
        if (errorMessage.isEmpty()) {
            reallySetUpdateIntervalInSeconds(updateIntervalInSeconds);
        }
    }

    /** Getter für den Abstand zwischen den Ausführungen des Tasks in Sekunden für die Gui. */
    int getUpdateIntervalInSeconds() {
        return updateIntervalInSeconds;
    }

    /** Setter für den Abstand zwischen den Ausführungen des Tasks in Sekunden für die Gui. */
    void setUpdateIntervalInSeconds(int updateIntervalInSeconds) {
        if (this.updateIntervalInSeconds != updateIntervalInSeconds) {
            if (updateIntervalInSeconds < minimalUpdateIntervalInSeconds
                    || updateIntervalInSeconds > maximalUpdateIntervalInSeconds) {
                throw new RuntimeException("Der übergebene Abstand zwischen den Ausführungen des "
                        + "Tasks in Sekunden liegt außerhalb des Zulässigen Bereichs!\n"
                        + "\t" + "Übergebener Abstand       : " + updateIntervalInSeconds + "\n"
                        + "\t" + "Minimal zulässiger Abstand: " + minimalUpdateIntervalInSeconds
                               + "\n"
                        + "\t" + "Maximal zulässiger Abstand: " + maximalUpdateIntervalInSeconds);
            }
            else {
                say("Start");
                cancelTimer();
                reallySetUpdateIntervalInSeconds(updateIntervalInSeconds);
                updateNow();
                say("Ende");
            }
        }
    }

    private void reallySetUpdateIntervalInSeconds(int updateIntervalInSeconds) {
        this.updateIntervalInSeconds = updateIntervalInSeconds;
        updateIntervalInSecondsChangeInformer.informAboutUpdateIntervalInSecondsChange(
                updateIntervalInSeconds);
    }

    /** Führt jetzt ein Update durch, nach Druck auf den Button in der Gui. */
    void updateNow() {
        say("Start");
        cancelTimer();
        update();
        say("Ende");
    }

    /**
     * Führt den Update jetzt durch und initiiert den nächsten Updateschritt nach dem
     * Update-Intervall.
     */
    private void update() {
        say("Start");
        runJob();
        say("nach checkDatabase()");
        scheduleNextTask();
        say("Ende");
    }

    private void runJob() {
        say("Start");
        checkForUpdateAfterDayChange();
        gui.startLongTimeProcess(jobDescritpion + " läuft");
        new Thread(() -> runJobInOwnThread()).start();
        say("Ende");
    }

    private void checkForUpdateAfterDayChange() {
        if (wasUpdated) {
            DateAndTime now = new DateAndTime();
            if (!now.getDate().equals(lastUpdateDateAndTime.getDate())) {
                updateAfterDayChangeInformer.informAboutUpdateAfterDayChange();
            }
        }
        else {
            wasUpdated = true;
        }
        lastUpdateDateAndTime = new DateAndTime();
    }

    private void runJobInOwnThread() {
        say("Start");
        DateAndTime startDateAndTime = new DateAndTime();
        StopWatch watch = new StopWatch();
        job.run();
        gui.jobDone(watch.getTime(), ++updateCount, startDateAndTime);
        say("Ende");
    }

    /** Beauftragt das nächste Ablaufen des Tasks. */
    private void scheduleNextTask() {
        say("Start");
        cancelTimer();
        gui.showIntervalTimeInSeconds(updateIntervalInSeconds);
        say("new Timer");
        say("Starte task in " + updateIntervalInSeconds + " Sekunden...");
        timer = new Timer();
        timer.schedule(defineTask(), 1000 * updateIntervalInSeconds);
        gui.restartCountDownWatch(updateIntervalInSeconds);
        say("Ende");
    }

    /** Definiert den Task. */
    private TimerTask defineTask() {
        return new TimerTask() {
            @Override
            public void run() {
                update();
            }
        };
        // Leider darf so ein Task immer nur einmal verwendet werden.
    }

    /**
     * Prüft, ob das Update-Intervall für den Zeitverbrauch zu klein war und passt es dann
     * entsprechend an.
     *
     * Zu klein ist das Update-Intervall dann, wenn der Job mehr als die Hälfte dieses Intervalls
     * benötigt.
     *
     * Wird aus der Gui aufgerufen, nachdem der Job beendet und der Prozess wieder im EDT ist.
     *
     * @param seconds
     *            Sekunden die der letzte Job benötigt hatte.
     */
    public synchronized void changeUpdateIntervalIfUpdateTookTooLong(long seconds) {
        if (changeUpdateIntervalIfUpdateTookTooLong) {
            say("Start");
            say("seconds = " + seconds);
            say("updateIntervalInSeconds / 2 = " + (updateIntervalInSeconds / 2));
            if (seconds > updateIntervalInSeconds / 2) {
                long newUpdateIntervalInSeconds = seconds * 10;
                if (newUpdateIntervalInSeconds > maximalUpdateIntervalInSeconds) {
                    updateIntervalInSeconds = maximalUpdateIntervalInSeconds;
                    say("Zu groß! Setze auf " + updateIntervalInSeconds);
                }
                else {
                    updateIntervalInSeconds = (int) newUpdateIntervalInSeconds;
                    say("Setze auf " + updateIntervalInSeconds);
                }
                scheduleNextTask();
            }
            say("Ende");
        }
    }

    /** Beendet das Programm. */
    @Override
    public void quit() {
        say("Start");
        cancelTimer();
        gui.quit();
        say("Ende");
    }

    private void cancelTimer() {
        say("Start");
        if (null != timer) {
            say("timer.cancel()");
            timer.cancel(); // Leider darf so ein Timer nach cancel nicht neu gescheduled werden.
            timer = null;   // daher kann er nicht final sein und muss über null geregelt werden.
        }
        say("Ende");
    }

    static void say(String message) {
        if (DEBUG) {
            DebugHelper.sayWithClassAndMethodAndTime(message);
        }
    }

}
