package de.duehl.basics.system.starter;

/*
 * Copyright 2023 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.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import de.duehl.basics.datetime.Timestamp;
import de.duehl.basics.io.FineFileWriter;
import de.duehl.basics.logging.Logger;
import de.duehl.basics.logging.NoLogger;
import de.duehl.basics.logic.ErrorHandler;

/**
 * Diese Klasse ruft eine cmd-Datei mit einem Programm auf und wartet auf die
 * Ausführung.
 * Sollte also nur aus einem Worker-Thread gestartet werden!
 *
 * @version 1.01     2023-03-13
 * @author Christian Dühl
 */

public class CmdStarter extends Starter {

    /** Gibt an, ob Ausgaben der Klasse nach STDOUT erwünscht sind. */
    private boolean verbose;

    /** Der Ausgabestrom als String, falls gespeichert. */
    private String outputText;

    /** Der Fehlerstrom als String, falls gespeichert. */
    private String errorText;

    /**
     * Konstruktor.
     *
     * @param error
     *            Objekt, das die Fehlerbehandlung durchführt.
     */
    public CmdStarter(ErrorHandler error) {
        this(new NoLogger(), error);
    }

    /**
     * Konstruktor.
     *
     * @param logger
     *            Der Logger für den Ablauf dieser Session.
     * @param error
     *            Objekt, das die Fehlerbehandlung durchführt.
     */
    public CmdStarter(Logger logger, ErrorHandler error) {
        super(logger, error);
        verbose = false;
        outputText = "";
        errorText = "";
    }

    /** Gibt an, dass Ausgaben der Klasse nach STDOUT erwünscht sind. */
    public void verbose() {
        verbose = true;
    }

    /**
     * Führt einen einzeiligen Befehl aus und wartet, bis es fertig ist.
     * Dieser wird mit cmd /c gestartet.
     *
     * @param program
     *            Auszuführender Befehl.
     * @param log
     *            Logfile, das erzeugt wird.
     * @param errorLog
     *            Extrakt der Fehler aus dem Logfile, wird ebenfalls erzeugt.
     * @return Gestarteter Aufruf oder der leere String, falls ein Fehler beim Start auftrat.
     */
    public String runAndWait(String program, String log, String errorLog) {
        log("Start, program = " + program + ", log = " + log + ", errorLog = " + errorLog);

        String[] params = createProgramStartParametersWithCmd(program);
        Process process = startProccessViaParams(params);
        if (null == process) {
            return ""; // es wurde nicht erfolgreich aufgerufen ...
        }

        waitForProcessAndWriteStreams(process, log, errorLog);

        log("Ende, program = " + program + ", log = " + log + ", errorLog = " + errorLog);
        return paramsToString(params); // Aufgerufenen Befehl als String zurückliefern.
    }

    /**
     * Führt einen einzeiligen Befehl aus und wartet, bis es fertig ist.
     * Dieser wird mit cmd /c gestartet.
     *
     * @param program
     *            Auszuführender Befehl.
     * @return Gestarteter Aufruf oder der leere String, falls ein Fehler beim Start auftrat.
     */
    public String runWaitAndStoreOutAndErrInStarter(String program) {
        log("Start, program = " + program);

        String[] params = createProgramStartParametersWithCmd(program);
        Process process = startProccessViaParams(params);
        if (null == process) {
            return ""; // es wurde nicht erfolgreich aufgerufen ...
        }

        waitForProcessAndStoreOutAndErrInStarter(process);

        log("Ende, program = " + program);
        return paramsToString(params); // Aufgerufenen Befehl als String zurückliefern.
    }

    /**
     * Führt einen einzeiligen Befehl aus und wartet, bis es fertig ist.
     *
     * @param program
     *            Auszuführender Befehl.
     * @param log
     *            Logfile, das erzeugt wird.
     * @param errorLog
     *            Extrakt der Fehler aus dem Logfile, wird ebenfalls erzeugt.
     * @return Gestarteter Aufruf oder der leere String, falls ein Fehler beim Start auftrat.
     */
    public String runAndWaitWithoutCmd(String program, String log, String errorLog) {
        log("Start, program = " + program + ", log = " + log + ", errorLog = " + errorLog);

        String[] params = createProgramStartParametersWithoutCmd(program);
        Process process = startProccessViaParams(params);
        if (null == process) {
            return ""; // es wurde nicht erfolgreich aufgerufen ...
        }

        waitForProcessAndWriteStreams(process, log, errorLog);

        log("Ende, program = " + program + ", log = " + log + ", errorLog = " + errorLog);
        return paramsToString(params); // Aufgerufenen Befehl als String zurückliefern.
    }

    /**
     * Führt eine Cmd-Datei aus und wartet NICHT, bis es fertig ist.
     *
     * @param program
     *            Auszuführendes Cmd-Programm.
     * @return Gestarteter Aufruf.
     */
    public String runAndForget(String program) {
        log("Start, program = " + program);

        String[] params = createProgramStartParametersWithCmd(program);
        Process process = startProccessViaParams(params);
        if (null == process) {
            return ""; // es wurde nicht erfolgreich aufgerufen ...
        }

        log("Ende, program = " + program);
        return paramsToString(params);
    }

    /**
     * Startet das Ausführen einer Cmd-Datei.
     *
     * @param program
     *            Auszuführendes Cmd-Programm.
     * @return Gestarteter Aufruf.
     */
    public Process startRun(String program) {
        log("Start, program = " + program);

        String[] params = createProgramStartParametersWithCmd(program);
        Process process = startProccessViaParams(params);

        log("Ende, program = " + program);
        return process;
    }

    /**
     * Startet das Ausführen einer Cmd-Datei.
     *
     * @param params
     *            Teile des zu startenden Aufrufs.
     * @return Gestarteter Aufruf.
     */
    public Process startRunWithoutCmd(String ... params) {
        log("Start, params = " + params);

        Process process = startProccessViaParams(params);

        log("Ende, params = " + params);
        return process;
    }

    private String[] createProgramStartParametersWithCmd(String program) {
        String[] params = {
                "cmd",
                "/c",
                program
        };
        return params;
    }

    private String[] createProgramStartParametersWithoutCmd(String program) {
        String[] params = {
                program
        };
        return params;
    }

    private Process startProccessViaParams(String[] params) {
        ProcessBuilder processBuilder = new ProcessBuilder(params);
        return startProccessViaBuilder(processBuilder);
    }

    private Process startProccessViaBuilder(ProcessBuilder processBuilder) {
        try {
            return processBuilder.start();
        }
        catch (IOException exception) {
            error.error("Es trat ein Fehler bei der Ausführung des Cmd-Prozesses auf.", exception);
            return null;
        }
    }

    /**
     * Wartet auf die Beendigung eines gestarteten Processes.
     *
     * @param process
     *            Gestarteter Prozess, auf den gewartet werden soll.
     * @param log
     *            Logfile, das erzeugt wird.
     * @param errorLog
     *            Extrakt der Fehler aus dem Logfile, wird ebenfalls erzeugt.
     */
    public void waitForProcessAndWriteStreams(Process process, String log, String errorLog) {
        log("Start, process = " + process + ", log = " + log + ", errorLog = " + errorLog);

        /* Auf Beendigung warten: */
        waitForProcess(process);

        /*
         * Falls der Process gewaltsam mit destroy() beendet wurde, ermitteln
         * wir keine Log- und Fehlerdateien:
         */
        if (0 != process.exitValue()) {
            return;
        }


        writeOutputStream(process, log);
        say("CmdStarter#waitForProcess der Prozess '" + process + "' STDOUT ermittelt.");

        writeErrorStream(process, errorLog);
        say("CmdStarter#waitForProcess der Prozess '" + process + "' STDERR ermittelt.");

        log("Ende, process = " + process + ", log = " + log + ", errorLog = " + errorLog);
    }

    /**
     * Wartet auf die Beendigung eines gestarteten Processes.
     *
     * @param process
     *            Gestarteter Prozess, auf den gewartet werden soll.
     */
    public void waitForProcessAndStoreOutAndErrInStarter(Process process) {
        log("Start, process = " + process);

        /* Auf Beendigung warten: */
        waitForProcess(process);

        /*
         * Falls der Process gewaltsam mit destroy() beendet wurde, ermitteln
         * wir keine Log- und Fehlerdateien:
         */
        if (0 != process.exitValue()) {
            return;
        }


        storeInputStream(process);
        say("CmdStarter#waitForProcess der Prozess '" + process + "' STDOUT ermittelt.");

        storeErrorStream(process);
        say("CmdStarter#waitForProcess der Prozess '" + process + "' STDERR ermittelt.");

        log("Ende, process = " + process);
    }

    /**
     * Wartet auf die Beendigung eines gestarteten Processes, ohne dessen Ausgaben nach STDOUT und
     * STDERR zu ermitteln.
     *
     * @param process
     *            Gestarteter Prozess, auf den gewartet werden soll.
     */
    public void waitForProcess(Process process) {
        log("Start, process = " + process);

        /* Auf Beendigung warten. */
        try {
            process.waitFor();
        }
        catch (InterruptedException e) {
            // nichts, das kann nicht unterbrochen werden.
        }
        say("CmdStarter#waitForProcess der Prozess '" + process + "' wurde beendet.");

        log("Ende, process = " + process);
    }

    private void writeOutputStream(Process process, String log) {
        BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
        FineFileWriter outWriter = new FineFileWriter(log);
        try {
            for (String line; (line = in.readLine()) != null;) {
                outWriter.writeln(line);
            }
        }
        catch (IOException exception) {
            error.error("Es trat ein Fehler bei der Ausführung des Cmd-Prozesses beim Ermitteln "
                    + "des Outputs auf.", exception);
        }
        outWriter.close();
    }

    private void writeErrorStream(Process process, String errorLog) {
        BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()));
        FineFileWriter errWriter = new FineFileWriter(errorLog);
        try {
            for (String line; (line = err.readLine()) != null;) {
                errWriter.writeln(line);
            }
        }
        catch (IOException exception) {
            error.error("Es trat ein Fehler bei der Ausführung des Cmd-Prozesses beim Ermitteln "
                    + "des Fehler-Outputs auf.", exception);
        }
        errWriter.close();
    }

    private void storeInputStream(Process process) {
        outputText = storeStreamAsText(process.getInputStream());
    }

    private void storeErrorStream(Process process) {
        errorText = storeStreamAsText(process.getErrorStream());
    }

    private String storeStreamAsText(InputStream stream) {
        StringBuilder builder = new StringBuilder();

        BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
        try {
            for (String line; (line = reader.readLine()) != null;) {
                builder.append(line).append("\n");
            }
        }
        catch (IOException exception) {
            error.error("Es trat ein Fehler bei der Ausführung des Cmd-Prozesses beim Ermitteln "
                    + "des Stroms auf.", exception);
        }
        return builder.toString();
    }

    private void say(String message) {
        if (verbose) {
            System.out.println(Timestamp.actualTime() + " " + message);
        }
    }

    /** Getter für den Ausgabestrom als String, falls gespeichert. */
    public String getOutputText() {
        return outputText;
    }

    /** Getter für den Fehlerstrom als String, falls gespeichert. */
    public String getErrorText() {
        return errorText;
    }

}
