package de.duehl.threads.timed;

/*
 * 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 static de.duehl.threads.debug.Debug.say;

/**
 * Diese Klasse startet eine Aufgabe (ein Runnable) und überwacht es in regelmäßigen Abständen.
 * Läuft sie zu lange, wird der Thread in dem die Aufgabe abgearbeitet wird abgebrochen.
 *
 * Diese Klasse ist für den einmaligen Gebrauch gedacht. Mehrfache Aufrufe von runThread() führen
 * zu einer RuntimeException.
 *
 * @version 1.01     2017-05-05
 * @author Christian Dühl
 */

public class TimedThreadRunner {

    /** Zustand der beobachteten Aufgabe. */
    public enum TaskState {
        NOT_STARTED,
        RUNNING,
        FINISHED_IN_TIME,
        CANCELLED
    }

    /** Wenn secondsBeforeKill oder mehr Sekunden vergangen sind, wird der Task beendet. */
    private final int secondsBeforeKill;

    /**
     * Alle secondsBetweenWatching Millisekunden wird der Thread, in dem die Aufgabe läuft, auf
     * Zeitüberschreitung überprüft.
     */
    private final long millisecondsBetweenWatching;

    /** Zu beobachtende Aufgabe. */
    private final Runnable task;

    /** Status der zu beobachtenden Aufgabe. */
    private TaskState taskState;

    /**
     * Konstruktor.
     *
     * @param secondsBeforeKill
     *            Wenn secondsBeforeKill oder mehr Sekunden vergangen sind, wird der Task beendet.
     * @param millisecondsBetweenWatching
     *            Alle millisecondsBetweenWatching Millisekunden wird der Thread, in dem die
     *            Aufgabe läuft, auf Zeitüberschreitung überprüft.
     * @param task
     *            Zu beobachtende Aufgabe.
     */
    public TimedThreadRunner(int secondsBeforeKill, long millisecondsBetweenWatching, Runnable task) {
        this.secondsBeforeKill = secondsBeforeKill;
        this.millisecondsBetweenWatching = millisecondsBetweenWatching;
        this.task = task;
        taskState = TaskState.NOT_STARTED;
    }

    /**
     * Start der Aufgabe in einem neuen Thread, alle secondsBetweenWatching Sekunden wird dieser
     * Thread überprüft.
     *
     * Wenn secondsBeforeKill oder mehr Sekunden vergangen sind, wird der Task beendet.
     *
     * @throws RuntimeException
     *             Wenn diese Methode mehr als einmal aufgerufen wird.
     */
    @SuppressWarnings("deprecation") // für Thread.stop()
    public void runTask() {
        checkTaskStateAtStart();

        /* Aufgabe starten: */
        long startTime = System.currentTimeMillis();
        Thread thread = new Thread(task);
        thread.start();

        /* Aufgabe überwachen: */
        while (taskState == TaskState.RUNNING) {
            sleep();

            /* Ist der Thread beendet, halten wir nur den Status fest: */
            if (!thread.isAlive()) {
                say("thread wurde während des letzten Wartens beendet.");
                taskState = TaskState.FINISHED_IN_TIME;
            }
            /* Anderenfalls überprüfen wir, ob die Zeit abgelaufen ist: */
            else {
                long now = System.currentTimeMillis();
                long secondsSinceStart = (now - startTime) / 1000L;
                if (secondsSinceStart >= secondsBeforeKill) {
                    say("Zeit abgelaufen! Thread muss abgewürgt werden...");
                    /*
                     * Auch wenn Thread.stop() deprecated ist, nutze ich es hier. Denn es gibt
                     * keinen Weg, eine aus dem Ruder gelaufene Reguläre-Ausdrucks-Maschine selbst
                     * zu stoppen.
                     *
                     * Für andere Aufgaben wäre es gut, statt runnable eine eigene Schnittstelle zu
                     * verwenden, die Runnable implementiert und eine Möglichkeit zum Anhalten
                     * anbietet.
                     *
                     * Der Aufrufer erfährt, dass der taskState CANCELLED ist und damit keinen
                     * Objekten im Zugriff der abgewürgten Aufgabe zu trauen ist.
                     */
                    thread.stop();
                    taskState = TaskState.CANCELLED;
                }
                else {
                    say("Abbruchzeit noch nicht erreicht:"
                            + "\n\tAbbruch nach: " + secondsBeforeKill
                            + "\n\tBisher      : " + secondsSinceStart);
                }
            }
        }
    }

    /**
     * Überprüft, ob der TaskState NOT_STARTED ist und setzt ihn dann auf RUNNING. Anderenfalls
     * wird eine Exception geworfen.
     *
     * @throws RuntimeException
     *             Wenn runTask() und damit auch diese Methode mehr als einmal aufgerufen wird.
     */
    private void checkTaskStateAtStart() {
        synchronized (this) {
            /*
             * Wäre dieser Block nicht synchronisiert, könnten zwei Threads erst beide feststellen,
             * dass der taskState NOT_STARTED ist und ihn dann auf RUNNING setzen.
             *
             * Auch wenn ein Objekt dieser Klasse zur Zeit nicht in mehreren Threads eingesetzt
             * werden soll, schadet diese Sicherheitsmaßnahme nichts.
             */
            if (taskState != TaskState.NOT_STARTED) {
                say("Prüfung zu Beginn fehlgeschlagen.");
                throw new RuntimeException("runThread() darf nicht gestartet werden, wenn bereits "
                        + "ein Thread beobachtet wird oder wurde. Bitte erzeugen Sie sich ein "
                        + "neues Objekt der Klasse " + this.getClass().toString() + ".");
            }
            taskState = TaskState.RUNNING;
        }
        say("Prüfung zu Beginn in Ordnung.");
    }

    /** Schläft secondsBetweenWatching Sekunden. */
    private void sleep() {
        say("Warte " + millisecondsBetweenWatching + " Millisekunden...");
        try {
            Thread.sleep(millisecondsBetweenWatching);
        }
        catch (InterruptedException e) {
            /*
             * Das dieser Thread, also der Thread des Aufrufers, unterbrochen wird, ist
             * unwahrscheinlich.
             * Falls doch, passiert aber auch nichts weiter schlimmes, dann wird einfach wieder
             * geprüft, ob die Aufgabe schon zu lange läuft.
             */
        }
    }

    /** Getter für den Zustand der Aufgabe. */
    public TaskState getTaskState() {
        return taskState;
    }

    /*
     * Die Frage ist, im Moment müsste ein Aufrufer warten, bis diese ein Objekt dieser Klasse
     * runTask() komplett ausgeführt hat und beendet wurde. Das ist für meinen Anwendungszweck
     * (nacheinander die Regeln anwenden) genau richtig.
     *
     * Ich könnte mir aber auch vorstellen, dass sich so eine Klasse einfach meldet, wenn der
     * Zustand nicht mehr RUNNING ist und dafür runTask() in einem extra Thread ausführt. Aber
     * lassen wir die Angelegenheit erstmal einfach.
     */

}
