/* BdfServer - Copyright (c) 2006-2024 Vincent Calame - Exemole
 * Logiciel libre donné sous triple licence :
 * 1) selon les termes de la CeCILL V2
 * 2) selon les termes de l’EUPL V.1.1
 * 3) selon les termes de la GNU GPLv3
 * Voir le fichier licences.txt
 */


package fr.exemole.bdfserver.tools.balayage.engine;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import net.fichotheque.exportation.balayage.BalayageOutput;
import net.fichotheque.exportation.balayage.BalayageUnit;
import net.mapeadores.util.annotation.Nullable;
import net.mapeadores.util.json.JSONWriter;
import net.mapeadores.util.json.JsonProperty;


/**
 *
 * @author Vincent Calame
 */
public class BalayageLog implements AutoCloseable, JsonProperty {

    public final static String UNRESOLVED_PATH = "unresolvedPath";
    public final static String WRONG_PATTERN = "wrongPattern";
    public final static String NO_OUTPUT = "noOutput";
    public final static String EXTRACTION_NOT_FOUND = "extractionNotFound";
    public final static String EXTRACTION_EXCEPTION = "extractionException";
    public final static String OUTPUTPATH_EXCEPTION = "outputPathException";
    public final static String TRANSFORMER_NOT_FOUND = "transformerNotFound";
    public final static String TRANSFORMER_EXCEPTION = "transformerException";
    public final static String NO_XSLTPATH = "noXsltPath";
    public final static String WRONG_PATH = "wrongPath";
    public final static String TRANSFORMATION_EXCEPTION = "transformerExceptionDuringTransformation";
    public final static String IO_EXCEPTION = "ioException";
    public final static String COMMANDCHECK_EXCEPTION = "commandCheck";
    public final static String COMMANDIO_EXCEPTION = "commandIOException";
    public final static String COMMANDINTERRUPTED_EXCEPTION = "commandInterruptedException";
    public final static String UNKNOWN_NAME = "unknownName";
    public final static String SQLEXPORT_ERROR = "sqlExportError";
    public final static String EXTRACTION_ATTRIBUTE = "extraction";
    public final static String SITEMAP_KEY = "sitemap";
    public final static String COMMAND_KEY = "command";
    public final static String SCRUTARIEXPORT_KEY = "scrutariexport";
    public final static String SQLEXPORT_KEY = "sqlexport";
    private final static DecimalFormat SecondFormat = new DecimalFormat("0.000");
    private final String balayageName;
    private final List<LogItem> logList;
    private final PrintWriter fileWriter;
    private long runStartTime;
    private long previousTime;
    private UnitInfo currentUnitInfo;
    private boolean hasError = false;

    public BalayageLog(String balayageName, File logFile) {
        this.balayageName = balayageName;
        this.logList = new ArrayList<LogItem>();
        PrintWriter pw;
        try {
            pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(logFile), "UTF-8"));
        } catch (IOException ioe) {
            pw = null;
        }
        this.fileWriter = pw;
    }

    public boolean hasError() {
        return hasError;
    }

    public void addBalayageUnitLog(int balayageUnitIndex, BalayageUnit balayageUnit) {
        getDuration();
        UnitInfo unitInfo = new UnitInfo(balayageUnitIndex, toBalayageUnitPath(balayageUnit));
        add(new BalayageUnitStart(unitInfo));
        currentUnitInfo = unitInfo;
    }

    public void addBalayageUnitError(int balayageUnitIndex, BalayageUnit balayageUnit, String errorType, String value, String message) {
        getDuration();
        hasError = true;
        add(new BalayageUnitError(balayageUnitIndex, toBalayageUnitPath(balayageUnit), errorType, value, message));
    }

    public void addOutputError(int balayageUnitIndex, int outputIndex, BalayageOutput output, String errorType, String value, String message) {
        getDuration(balayageUnitIndex);
        hasError = true;
        add(new OutputError(balayageUnitIndex, toBalayageUnitPath(output.getBalayageUnit()), outputIndex, errorType, value, message));
    }

    public void addPostscriptumLog(String postscriptumType, String name) {
        long duration = getDuration();
        add(new PostscriptumLog(postscriptumType, name, duration));
    }

    public void addPostscriptumError(String postscriptumType, String name, String errorType, String message) {
        getDuration();
        hasError = true;
        add(new PostscriptumError(postscriptumType, name, errorType, message));
    }

    public void addFileGeneration(int balayageUnitIndex, int outputIndex, File f) {
        long duration = getDuration(balayageUnitIndex);
        LogItem logItem = new FileGeneration(balayageUnitIndex, outputIndex, f, duration);
        printToFile(logItem);
    }

    public void runStarted() {
        runStartTime = System.currentTimeMillis();
        previousTime = runStartTime;
    }

    public void runEnded() {
        long duration = getDuration();
        add(new End(System.currentTimeMillis() - runStartTime));
    }

    private void printToFile(LogItem logItem) {
        if (fileWriter != null) {
            logItem.print(fileWriter);
        }
    }

    private long getDuration() {
        return getDuration(-1);
    }

    private long getDuration(int balayageUnitIndex) {
        long currentTime = System.currentTimeMillis();
        long duration = currentTime - previousTime;
        previousTime = currentTime;
        if ((currentUnitInfo != null) && (currentUnitInfo.index != balayageUnitIndex)) {
            long unitDuration = currentTime - currentUnitInfo.startTime;
            add(new BalayageUnitEnd(currentUnitInfo, unitDuration));
            currentUnitInfo = null;
        }
        return duration;
    }

    private void add(LogItem logItem) {
        logList.add(logItem);
        printToFile(logItem);
    }

    @Override
    public void close() {
        if (fileWriter != null) {
            fileWriter.close();
        }
    }

    public void printLog(PrintWriter pw) {
        for (LogItem logItem : logList) {
            logItem.print(pw);
        }
    }

    @Override
    public String getName() {
        return "balayageLog";
    }

    @Override
    public void writeValue(JSONWriter jw) throws IOException {
        jw.object();
        {
            jw.key("balayageName")
                    .value(balayageName);
            jw.key("itemArray");
            jw.array();
            for (LogItem logItem : logList) {
                jw.object();
                {
                    jw.key("type")
                            .value(logItem.getType());
                    logItem.properties(jw);
                }
                jw.endObject();
            }
            jw.endArray();
        }
        jw.endObject();
    }


    private static abstract class LogItem {

        public abstract String getType();

        public abstract void print(PrintWriter pw);

        public abstract void properties(JSONWriter jw) throws IOException;

    }


    private static class End extends LogItem {

        private final long duration;

        private End(long duration) {
            this.duration = duration;
        }

        @Override
        public String getType() {
            return "end";
        }

        @Override
        public void print(PrintWriter pw) {
            pw.println("");
            pw.print("===== Generation time  : ");
            printTime(pw, duration);
            pw.println("");
        }

        @Override
        public void properties(JSONWriter jw) throws IOException {
            jw.key("duration")
                    .value(duration);
        }

    }


    private static class BalayageUnitStart extends LogItem {

        private final UnitInfo unitInfo;
        private final int freeMemory;


        private BalayageUnitStart(UnitInfo unitInfo) {
            this.unitInfo = unitInfo;
            this.freeMemory = (int) Math.floor(((double) Runtime.getRuntime().freeMemory()) / (1024 * 1024));
        }

        @Override
        public String getType() {
            return "balayage-unit-start";
        }

        @Override
        public void print(PrintWriter pw) {
            pw.println("");
            pw.print("+------- ");
            pw.print("balayageUnit ");
            pw.print(unitInfo.number);
            pw.print(": ");
            pw.print(unitInfo.path);
            pw.println(") / mem = " + freeMemory);
        }

        @Override
        public void properties(JSONWriter jw) throws IOException {
            unitInfo.write(jw);
        }

    }


    private static class BalayageUnitEnd extends LogItem {

        private final UnitInfo unitInfo;
        private final long duration;

        private BalayageUnitEnd(UnitInfo unitInfo, long duration) {
            this.unitInfo = unitInfo;
            this.duration = duration;
        }

        @Override
        public String getType() {
            return "balayage-unit-end";
        }

        @Override
        public void print(PrintWriter pw) {
            pw.print("+------- ");
            pw.print(unitInfo.number);
            pw.print(" / ");
            printTime(pw, duration);
            pw.println("");
        }

        @Override
        public void properties(JSONWriter jw) throws IOException {
            unitInfo.write(jw);
            writeDuration(jw, duration);
        }

    }


    private static class BalayageUnitError extends LogItem {

        private final int balayageUnitIndex;
        private final String balayageUnitPath;
        private final String message;
        private final String errorType;
        private final String value;


        private BalayageUnitError(int balayageUnitIndex, String balayageUnitPath, String errorType, String value, String message) {
            this.balayageUnitIndex = balayageUnitIndex;
            this.message = message;
            this.errorType = errorType;
            this.value = value;
            this.balayageUnitPath = balayageUnitPath;
        }

        @Override
        public String getType() {
            return "balayage-unit-error";
        }

        @Override
        public void print(PrintWriter pw) {
            pw.println("");
            pw.print("[! ");
            pw.print(errorType);
            pw.print(" !] ");
            pw.print("balayageUnit ");
            pw.print(balayageUnitIndex + 1);
            pw.print(": ");
            pw.print(balayageUnitPath);
            if (value != null) {
                pw.print(", value: ");
                pw.print(value);
            }
            if (message != null) {
                pw.print(", message: ");
                pw.print(message);
            }
            pw.println("");

        }

        @Override
        public void properties(JSONWriter jw) throws IOException {
            writeUnit(jw, balayageUnitIndex + 1, balayageUnitPath);
            writeError(jw, errorType, value, message);
        }

    }


    private static class OutputError extends LogItem {

        private final int balayageUnitIndex;
        private final String balayageUnitPath;
        private final int outputIndex;
        private final String errorType;
        private final String message;
        private final String value;


        private OutputError(int balayageUnitIndex, String balayageUnitPath, int outputIndex, String errorType, String value, String message) {
            this.balayageUnitIndex = balayageUnitIndex;
            this.outputIndex = outputIndex;
            this.message = message;
            this.errorType = errorType;
            this.value = value;
            this.balayageUnitPath = balayageUnitPath;
        }

        @Override
        public String getType() {
            return "output-error";
        }

        @Override
        public void print(PrintWriter pw) {
            pw.println("");
            pw.print("[! ");
            pw.print(errorType);
            pw.print(" !] ");
            pw.print("balayageUnit ");
            pw.print(balayageUnitIndex + 1);
            pw.print(": ");
            pw.print(balayageUnitPath);
            pw.print(", output: ");
            pw.print(outputIndex + 1);
            if (value != null) {
                pw.print(", value: ");
                pw.print(value);
            }
            if (message != null) {
                pw.print(", message: ");
                pw.print(message);
            }
            pw.println("");

        }

        @Override
        public void properties(JSONWriter jw) throws IOException {
            writeUnit(jw, balayageUnitIndex + 1, balayageUnitPath);
            jw.key("output")
                    .value(outputIndex + 1);
            writeError(jw, errorType, value, message);
        }

    }


    private static class PostscriptumLog extends LogItem {

        private final String type;
        private final String name;
        private final long duration;

        private PostscriptumLog(String type, String name, long duration) {
            this.type = type;
            this.name = name;
            this.duration = duration;
        }

        @Override
        public String getType() {
            return "postscriptum-log";
        }

        @Override
        public void print(PrintWriter pw) {
            pw.println("");
            pw.print("+------- ");
            pw.print(type);
            pw.print(": ");
            pw.println(name);
        }

        @Override
        public void properties(JSONWriter jw) throws IOException {
            writePostscriptum(jw, type, name);
            writeDuration(jw, duration);
        }

    }


    private static class PostscriptumError extends LogItem {

        private final String postscriptumType;
        private final String name;
        private final String errorType;
        private final String message;

        private PostscriptumError(String postscriptumType, String name, String errorType, String message) {
            this.postscriptumType = postscriptumType;
            this.name = name;
            this.errorType = errorType;
            this.message = message;
        }

        @Override
        public String getType() {
            return "postscriptum-error";
        }

        @Override
        public void print(PrintWriter pw) {
            pw.println("");
            pw.print("[! ");
            pw.print(postscriptumType);
            pw.print(": ");
            pw.print(errorType);
            pw.print(" !] ");
            pw.print(message);
            pw.println("");

        }

        @Override
        public void properties(JSONWriter jw) throws IOException {
            writePostscriptum(jw, postscriptumType, name);
            writeError(jw, errorType, null, message);
        }

    }


    private static class FileGeneration extends LogItem {

        private final int balayageUnitIndex;
        private final int outputIndex;
        private final String path;
        private final long duration;
        private final int freeMemory;

        private FileGeneration(int balayageUnitIndex, int outputIndex, File f, long duration) {
            this.balayageUnitIndex = balayageUnitIndex;
            this.outputIndex = outputIndex;
            this.duration = duration;
            this.path = f.getAbsolutePath();
            this.freeMemory = (int) Math.floor(((double) Runtime.getRuntime().freeMemory()) / (1024 * 1024));
        }

        @Override
        public String getType() {
            return "file-generation";
        }

        @Override
        public void print(PrintWriter pw) {
            pw.print("out: ");
            pw.print(balayageUnitIndex + 1);
            pw.print(".");
            pw.print(outputIndex + 1);
            pw.print(" path: ");
            pw.print(path);
            pw.print(" (");
            printTime(pw, duration);
            pw.println(") / mem = " + freeMemory);
        }

        @Override
        public void properties(JSONWriter jw) throws IOException {
            StringBuilder buf = new StringBuilder();
            buf.append(balayageUnitIndex + 1);
            buf.append(".");
            buf.append(outputIndex + 1);
            jw.key("step")
                    .value(buf.toString());
            jw.key("path")
                    .value(path);
            writeDuration(jw, duration);
        }

    }

    private static void printTime(PrintWriter pw, long time) {
        float f = ((float) time) / 1000;
        pw.print(String.valueOf(f));
        pw.print("s");
    }

    private static String toBalayageUnitPath(BalayageUnit balayageUnit) {
        StringBuilder buf = new StringBuilder();
        buf.append(balayageUnit.getType());
        String extractionPath = balayageUnit.getExtractionPath();
        if (!extractionPath.isEmpty()) {
            buf.append("/");
            buf.append(extractionPath);
        }
        return buf.toString();
    }

    private static void writeDuration(JSONWriter jw, long duration) throws IOException {
        jw.key("duration")
                .value(SecondFormat.format(((double) duration) / 1000));
    }

    private static void writeUnit(JSONWriter jw, int number, String path) throws IOException {
        jw.key("unit");
        jw.object();
        {
            jw.key("number")
                    .value(number);
            jw.key("path")
                    .value(path);
        }
        jw.endObject();
    }

    private static void writePostscriptum(JSONWriter jw, String type, String name) throws IOException {
        jw.key("postscriptum");
        jw.object();
        {
            jw.key("type")
                    .value(type);
            jw.key("name")
                    .value(name);
        }
        jw.endObject();
    }

    private static void writeError(JSONWriter jw, String errorType, @Nullable String value, @Nullable String message) throws IOException {
        jw.key("error");
        jw.object();
        {
            jw.key("type")
                    .value(errorType);
            if (value != null) {
                jw.key("value")
                        .value(value);
            }
            if (message != null) {
                jw.key("message")
                        .value(message);
            }
        }
        jw.endObject();
    }


    private static class UnitInfo {

        private final int index;
        private final int number;
        private final String path;
        private final long startTime;

        private UnitInfo(int index, String path) {
            this.index = index;
            this.number = index + 1;
            this.path = path;
            this.startTime = System.currentTimeMillis();
        }

        private void write(JSONWriter jw) throws IOException {
            writeUnit(jw, number, path);
        }

    }

}
