package de.duehl.swing.ui.error;

/*
 * Copyright 2022 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.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Image;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.filechooser.FileFilter;

import de.duehl.swing.ui.GuiTools;
import de.duehl.swing.ui.dialogs.base.ModalDialogBase;
import de.duehl.basics.datetime.Timestamp;
import de.duehl.basics.io.FineFileReader;
import de.duehl.basics.io.FineFileWriter;
import de.duehl.basics.logging.Logger;
import de.duehl.basics.system.ExceptionHelper;
import de.duehl.basics.system.SystemTools;
import de.duehl.basics.text.html.HtmlTool;

/**
 * Diese Klasse stellt einen Dialog zur Anzeige eines Fehlers dar.
 *
 * @version 1.04     2022-03-08
 * @author Christian Dühl
 */

public class ErrorDialog extends ModalDialogBase {

    private static final int WIDTH = 850;

    private static final boolean SHOW_EXCEPTION_NOTE = false;

    /** Rahmen der Oberfläche, vor der dieser Dialog erzeugt wird. */
    private final Point parentLocation;

    /** Text, der den Fehler beschreibt. */
    private final String errorText;

    /** Exception, die aufgetreten ist. */
    private final Throwable exception;

    /** Image (Icon) des Programms. */
    private final Image programImage;

    /** Vordergrundfarbe */
    private final Color foregroundColor;

    /** Hintergrundfarbe */
    private final Color backgroundColor;

    /** Name der Log-Datei. */
    private String logFileName;

    /**
     * Gibt an, ob es sich um einen logischen Programmfehler (true) oder um einen Fehler, der eine
     * Exception ausgelöst hat, handelt.
     */
    private final boolean logicalError;

    private final StackTraceElement[] stackTraceElements;

    /** Zuletzt geschriebene Zeile des Logfiles. */
    private int lastWrittenLineNumber;

    /* Gui Elemente: */
    private JPanel detailPanel;
    private boolean showDetails;
    private JButton detailsButton;
    private JScrollPane stackTraceScroll;

    /**
     * Konstruktor
     *
     * @param errorText
     *            Text, der den Fehler beschreibt.
     */
    public ErrorDialog(String errorText) {
        this(errorText, null, null, null, null, null, null, -1);
    }

    /**
     * Konstruktor
     *
     * @param errorText
     *            Text, der den Fehler beschreibt.
     * @param parentLocation
     *            Lokation des Fensters, vor dem der Fehlerdialog angezeigt wird.
     */
    public ErrorDialog(String errorText, Point parentLocation) {
        this(errorText, null, null, null, null, null, parentLocation, -1);
    }

    /**
     * Konstruktor
     *
     * @param errorText
     *            Text, der den Fehler beschreibt.
     * @param exception
     *            Exception, die aufgetreten ist.
     */
    public ErrorDialog(String errorText, Throwable exception) {
        this(errorText, exception, null, null, null, null, new Point(300, 50), -1);
    }

    /**
     * Konstruktor
     *
     * @param errorText
     *            Text, der den Fehler beschreibt.
     * @param exception
     *            Exception, die aufgetreten ist.
     * @param parentLocation
     *            Lokation des Fensters, vor dem der Fehlerdialog angezeigt wird.
     */
    public ErrorDialog(String errorText, Throwable exception, Point parentLocation) {
        this(errorText, exception, null, null, null, null, parentLocation, -1);
    }

    /**
     * Konstruktor
     *
     * @param errorText
     *            Text, der den Fehler beschreibt.
     * @param exception
     *            Exception, die aufgetreten ist.
     * @param programImage
     *            ImageIcon des Programms.
     * @param parentLocation
     *            Lokation des Fensters, vor dem der Fehlerdialog angezeigt wird.
     */
    public ErrorDialog(String errorText, Throwable exception, Image programImage,
            Point parentLocation) {
        this(errorText, exception, programImage, null, null, null, parentLocation, -1);
    }

    /**
     * Konstruktor
     *
     * @param errorText
     *            Text, der den Fehler beschreibt.
     * @param foregroundColor
     *            Vordergrundfarbe.
     * @param backgroundColor
     *            Hintergrundfarbe.
     * @param parentLocation
     *            Lokation des Fensters, vor dem der Fehlerdialog angezeigt wird.
     */
    public ErrorDialog(String errorText, Color foregroundColor, Color backgroundColor,
            Point parentLocation) {
        this(errorText, null, null, null, foregroundColor, backgroundColor, parentLocation, -1);
    }

    /**
     * Konstruktor
     *
     * @param errorText
     *            Text, der den Fehler beschreibt.
     * @param exception
     *            Exception, die aufgetreten ist.
     * @param foregroundColor
     *            Vordergrundfarbe.
     * @param backgroundColor
     *            Hintergrundfarbe.
     * @param parentLocation
     *            Lokation des Fensters, vor dem der Fehlerdialog angezeigt wird.
     */
    public ErrorDialog(String errorText, Throwable exception,
            Color foregroundColor, Color backgroundColor, Point parentLocation) {
        this(errorText, exception, null, null, foregroundColor, backgroundColor, parentLocation, -1);
    }

    /**
     * Konstruktor
     *
     * @param errorText
     *            Text, der den Fehler beschreibt.
     * @param programImage
     *            ImageIcon des Programms.
     * @param logFileName
     *            Name der Logdatei.
     * @param foregroundColor
     *            Vordergrundfarbe.
     * @param backgroundColor
     *            Hintergrundfarbe.
     * @param parentLocation
     *            Lokation des Fensters, vor dem der Fehlerdialog angezeigt wird.
     */
    public ErrorDialog(String errorText, Image programImage, String logFileName,
            Color foregroundColor, Color backgroundColor, Point parentLocation) {
        this(errorText, null, programImage, logFileName, foregroundColor, backgroundColor,
                parentLocation, -1);
    }

    /**
     * Konstruktor
     *
     * @param errorText
     *            Text, der den Fehler beschreibt.
     * @param exception
     *            Exception, die aufgetreten ist.
     * @param programImage
     *            ImageIcon des Programms.
     * @param logFileName
     *            Name der Logdatei.
     * @param parentLocation
     *            Lokation des Fensters, vor dem der Fehlerdialog angezeigt wird.
     */
    public ErrorDialog(String errorText, Throwable exception, Image programImage,
            String logFileName, Point parentLocation) {
        this(errorText, exception, programImage, logFileName, null, null, parentLocation, -1);
    }

    /**
     * Konstruktor
     *
     * @param errorText
     *            Text, der den Fehler beschreibt.
     * @param exception
     *            Exception, die aufgetreten ist.
     * @param programImage
     *            ImageIcon des Programms.
     * @param logFileName
     *            Name der Logdatei.
     * @param foregroundColor
     *            Vordergrundfarbe.
     * @param backgroundColor
     *            Hintergrundfarbe.
     * @param parentLocation
     *            Lokation des Fensters, vor dem der Fehlerdialog angezeigt wird.
     */
    public ErrorDialog(String errorText, Throwable exception, Image programImage,
            String logFileName, Color foregroundColor, Color backgroundColor, Point parentLocation) {
        this(errorText, exception, programImage, logFileName, foregroundColor, backgroundColor,
                parentLocation, -1);
    }

    /**
     * Konstruktor
     *
     * @param errorText
     *            Text, der den Fehler beschreibt.
     * @param exception
     *            Exception, die aufgetreten ist.
     * @param programImage
     *            Image (Icon) des Programms.
     * @param logFileName
     *            Name der Logdatei.
     * @param foregroundColor
     *            Vordergrundfarbe.
     * @param backgroundColor
     *            Hintergrundfarbe.
     * @param parentLocation
     *            Lokation des Fensters, vor dem der Fehlerdialog angezeigt wird.
     * @param lastWrittenLineNumber
     *            Nummer der zuletzt geschriebenen Zeile.
     */
    public ErrorDialog(String errorText, Throwable exception, Image programImage,
            String logFileName, Color foregroundColor, Color backgroundColor, Point parentLocation,
            int lastWrittenLineNumber) {
        super("");
        addEscapeBehaviour();

        this.errorText = errorText;
        this.exception = exception;
        this.programImage = programImage;
        this.logFileName = logFileName;
        if (null == foregroundColor || null == backgroundColor) {
            JPanel aPanel = new JPanel();
            this.foregroundColor = aPanel.getForeground();
            this.backgroundColor = aPanel.getBackground();
        }
        else {
            this.foregroundColor = foregroundColor;
            this.backgroundColor = backgroundColor;
        }

        this.parentLocation = parentLocation;
        this.lastWrittenLineNumber = lastWrittenLineNumber;

        System.out.println("Behandelter Fehler:\n" + errorText + "\n");
        if (null == exception) {
            logicalError = true;
            /*
             * Da die ersten drei Informationen des Traces irreführend sind,
             * werden sie verworfen:
             */
            StackTraceElement[] ste = Thread.currentThread().getStackTrace();
            int length = ste.length;
            stackTraceElements = new StackTraceElement[length-3];
            for (int i=3; i<length; ++i) {
                stackTraceElements[i-3] = ste[i];
            }
        }
        else {
            logicalError = false;
            exception.printStackTrace(System.out);
            stackTraceElements = exception.getStackTrace();
        }

        fillDialog();
    }

    public void addLogger(Logger logger) {
        logFileName = logger.getLogFileName();
        lastWrittenLineNumber = logger.getLastWrittenLineNumber();
    }

    /** Baut die Gui auf. */
    @Override
    protected void populateDialog() {
        setTitle("Es ist ein "
                + (logicalError ? "normaler Fehler" : "Fehler mit Exception")
                + " aufgetreten!");
        setColors(getDialog());

        /* Icon setzen: */
        if (null != programImage) {
            getWindow().setIconImage(programImage);
        }

        add(createErrorPanel(), BorderLayout.NORTH);
        add(createDetailPanel(), BorderLayout.CENTER);
        add(createQuitButton(), BorderLayout.SOUTH);

        setLocation();
        if (!logicalError) {
            toggleDetails(); // Exception anzeigen, wenn da!
        }
    }

    private void setLocation() {
        if (parentLocation != null) {
            int x = (int) parentLocation.getX();
            int y = (int) parentLocation.getY();
            setLocation(x + 150, y + 90);
        }
        else {
            setLocation(350, 300);
        }
    }

    /**
     * Erzeugt den oberen Bereich mit dem Fehlertext und der Möglichkeit, die Details ein- oder
     * auszuschalten.
     */
    private JPanel createErrorPanel() {
        JPanel panel = new JPanel();
        setColors(panel);
        panel.setLayout(new BorderLayout());
        GuiTools.createTitle(panel);

        panel.add(createErrorDummy(), BorderLayout.NORTH);
        if (SHOW_EXCEPTION_NOTE) {
            panel.add(createErrorExceptionNote(), BorderLayout.WEST);
        }
        panel.add(createErrorText(), BorderLayout.CENTER);
        panel.add(createErrorDetailButton(), BorderLayout.EAST);

        return panel;
    }

    /** Erzeugt den Dummy Panel zur Erzeugung der richtigen Breite. */
    private Component createErrorDummy() {
        JPanel panel = new JPanel();
        setColors(panel);
        panel.setLayout(new BorderLayout());

        panel.setPreferredSize(new Dimension(WIDTH, 0));
        panel.setMaximumSize(new Dimension(WIDTH, 0));

        return panel;
    }

    /** Erzeugt den Hinweis auf die Ausnahme, falls eine aufgetreten ist. */
    private Component createErrorExceptionNote() {
        JPanel panel = new JPanel();
        setColors(panel);
        panel.setLayout(new BorderLayout());
        GuiTools.createTitle(panel);

        JLabel label = new JLabel();
        label.setBorder(new EmptyBorder(5, 5, 5, 5));
        setColors(label);
        String text;
        if (logicalError) {
            text = "Es ist ein<br>normaler Fehler<br>aufgetreten!";
        }
        else {
            text = "Es ist eine<br>Ausnahme<br>aufgetreten!";
            label.setForeground(Color.RED);
        }
        label.setText("<html>" + text + "</html>");
        panel.add(label, BorderLayout.CENTER);

        return panel;
    }

    /** Erzeugt die Anzeige der Fehlermeldung. */
    private Component createErrorText() {
        JPanel panel = new JPanel();
        setColors(panel);
        panel.setLayout(new BorderLayout());
        GuiTools.createTitle(panel);

        JLabel errorLabel = new JLabel(HtmlTool.htmlify(errorText, 60));
        errorLabel.setOpaque(true); // sonst wird der Hintergrund nicht gefärbt, warum auch immer
                                    // der gefärbte Panel keinen hat...
        setMonospacedFont(errorLabel, 18);
        setColors(errorLabel);

        errorLabel.setForeground(Color.RED);
        errorLabel.setBorder(new EmptyBorder(10, 20, 10, 20));
        JScrollPane scroll = new JScrollPane(errorLabel);
        //scroll.setMaximumSize(new Dimension(400, 900));
        scroll.setPreferredSize(new Dimension(500, 200));
        panel.add(scroll, BorderLayout.CENTER);

        return panel;
    }

    /**
     * Setzt die Schriftart der Komponente auf Monospaced.
     *
     * @param component
     *            Zu ändernden Komponente.
     * @param fontSize
     *            Schriftgröße.
     */
    private void setMonospacedFont(Component component, int fontSize) {
        //Font font = component.getFont();
        component.setFont(new Font("Monospaced", Font.BOLD, fontSize));
    }

    /** Erzeugt den Button zum Anzeigen oder Verstecken der Details. */
    private Component createErrorDetailButton() {
        JPanel panel = new JPanel();
        setColors(panel);
        panel.setLayout(new BorderLayout());
        GuiTools.createTitle(panel);

        JButton detailsButton = new JButton("Details anzeigen");
        detailsButton.setPreferredSize(new Dimension(150, 50));
        setColors(detailsButton);
        //detailsButton.setBorder(BorderFactory.createRaisedBevelBorder());
        detailsButton.setFocusable(false);
        detailsButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent event) {
                toggleDetails();
            }
        });
        panel.add(detailsButton, BorderLayout.CENTER);
        this.detailsButton = detailsButton;

        return panel;
    }

    /** Erstellt den mittleren Bereich mit den Details. */
    private Component createDetailPanel() {
        JPanel panel = new JPanel();
        setColors(panel);
        panel.setLayout(new BorderLayout());

        detailPanel = panel;

        /* Befüllt wird in toggleDetail() ... */

        return panel;
    }

    /** Erzeugt den Quit-Button. */
    private JPanel createQuitButton() {
        JPanel lowerPanel = new JPanel();
        setColors(lowerPanel);
        lowerPanel.setLayout(new BorderLayout());

        JButton quitButton = new JButton("Ok");
        quitButton.setPreferredSize(new Dimension(90, 40));
        setColors(quitButton);
        quitButton.setFocusable(false);
        quitButton.addActionListener(e -> closeDialog());
        lowerPanel.add(quitButton, BorderLayout.EAST);

        SwingUtilities.invokeLater(() -> quitButton.requestFocus());

        return lowerPanel;
    }

    /** Schaltet die Details um und zeigt sie ggf. an. */
    private void toggleDetails() {
        if (showDetails) {
            detailPanel.setPreferredSize(new Dimension(WIDTH, 0));
            detailPanel.removeAll();

            detailsButton.setText("Details anzeigen");
        }
        else {
            detailPanel.setPreferredSize(new Dimension(WIDTH, 500));
            detailPanel.add(createInnerDetails(), BorderLayout.CENTER);
            detailPanel.add(createReportButton(), BorderLayout.SOUTH);

            detailsButton.setText("Details verbergen");
        }
        showDetails = !showDetails;

        pack();
        validate();
        repaint();

        scrollUp();
    }

    /** Erstellt die inneren Details. */
    private Component createInnerDetails() {
        JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
                createExceptionMessage(), createStackTrace());
        splitPane.setDividerLocation(100);
        setColors(splitPane);

        scrollUp();

        return splitPane;
    }

    /**
     * Erstellt den Bereich für die Message der Exception, oder die Anzeige, dass ein logischer
     * Fehler aufgetreten ist.
     */
    private Component createExceptionMessage() {
        JPanel panel = new JPanel();
        setColors(panel);
        panel.setLayout(new BorderLayout());
        GuiTools.createTitle(panel);

        String text;
        if (logicalError) {
            text = "Es ist ein logischer Fehler aufgetreten. "
                    + "Ablauf des Programms bis zur Fehlerstelle:";
        }
        else {
            text = exception.getClass().getName() + ": "
                    + exception.getMessage();
        }
        JLabel messageLabel = new JLabel(HtmlTool.htmlify(text, 80));
        setMonospacedFont(messageLabel, 14);
        setColors(messageLabel);
        messageLabel.setForeground(Color.RED);

//        JTextArea headerTextArea = new JTextArea();
//        setColors(headerTextArea);
//        headerTextArea.setEditable(false);
//        //headerTextArea.setFocusable(false);
//        //headerTextArea.setOpaque(false);
//        System.out.println("backgroundColor = " + backgroundColor);
//        //headerTextArea.set
//        headerTextArea.setText(text);

        panel.add(messageLabel, BorderLayout.CENTER);

        JScrollPane scroll = new JScrollPane(panel);
        scroll.setPreferredSize(new Dimension(100, 100));

        return scroll;
    }

    /** Erstellt die Anzeige des Stacktrace. */
    private Component createStackTrace() {
        JPanel panel = new JPanel();
        setColors(panel);
        panel.setLayout(new BorderLayout());
        GuiTools.createTitle(panel);

        JLabel stackTraceLabel = new JLabel(createStackTraceText());
        setMonospacedFont(stackTraceLabel, 13);
        setColors(stackTraceLabel);
        stackTraceLabel.setVerticalAlignment(JLabel.NORTH);
        panel.add(stackTraceLabel, BorderLayout.CENTER);

        stackTraceScroll = new JScrollPane(panel);
        setColors(stackTraceScroll);
        scrollUp();

        return stackTraceScroll;
    }

    private String createStackTraceText() {
        StringBuilder builder = new StringBuilder();

        addOriginalStacktraceToStackTraceOutput(builder);
        addCausesToStackTraceOutput(builder);
        String text = HtmlTool.htmlify(builder.toString());

        return text;
    }

    private void addOriginalStacktraceToStackTraceOutput(StringBuilder builder) {
        addStacktraceToStackTraceOutput(builder, stackTraceElements);
    }

    private void addStacktraceToStackTraceOutput(StringBuilder builder, StackTraceElement[] stes) {
        for (StackTraceElement element : stes) {
            builder.append(element.toString());
            builder.append("\n");
        }
    }

    private void addCausesToStackTraceOutput(StringBuilder builder) {
        if (null != exception) {
            Throwable cause = exception.getCause();
            while (cause != null) {
                addOneCouseToStackTraceOutput(builder, cause);
                cause = cause.getCause();
            }
        }
    }

    private void addOneCouseToStackTraceOutput(StringBuilder builder, Throwable cause) {
        builder.append("\n");
        builder.append(HtmlTool.red("Caused by:\n"));
        builder.append(HtmlTool.red(cause.getClass().getName() + ": "));
        builder.append(HtmlTool.red(cause.getMessage()) + "\n");
        addStacktraceToStackTraceOutput(builder, cause.getStackTrace());
    }

    /** Scrollt den Stacktrace nach oben. */
    private void scrollUp() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                if (null != stackTraceScroll) {
                    int minimum = stackTraceScroll.getVerticalScrollBar().getMinimum();
                    stackTraceScroll.getVerticalScrollBar().setValue(minimum);
                }
            }
        });
    }

    /** Erzeugt den Button zum Speichern des Berichts. */
    private Component createReportButton() {
        JPanel panel = new JPanel();
        setColors(panel);
        panel.setLayout(new BorderLayout());
        GuiTools.createTitle(panel);

        JButton reportButton = new JButton("Bericht");
        setColors(reportButton);
        reportButton.setFocusable(false);
        reportButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent arg0) {
                report();
            }
        });
        panel.add(reportButton, BorderLayout.CENTER);

        return panel;
    }

    /**
     * Erzeugt einen Bericht aus Fehlertext, Exception, eventuell projektspezifischen weiteren
     * Daten wie zum Beispiel einem Graphen und Log, so vorhanden.
     */
    private void report() {
        StringBuilder builder = new StringBuilder();
        builder.append("Fehlerbericht " + Timestamp.fullTimestamp() + ":\n");
        builder.append("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
        builder.append("Fehlertext:\n");
        builder.append(errorText + "\n");
        if (logicalError) {
            builder.append("\n\nEs ist ein logischer Fehler aufgetreten.\n");
            builder.append("\n\nStacktrace des Programmablaufes bis zu dieser Stelle:\n");
            addOriginalStacktraceToStackTraceOutput(builder);
        }
        else {
            builder.append("\n\nEs ist eine Ausnahme aufgetreten.\n");
            /*
            builder.append("\n\nException Klasse:\n");
            builder.append(exception.getClass().getName() + "\n");
            builder.append("\n\nException Meldung:\n");
            builder.append(exception.getMessage());
            builder.append("\n\nException Stacktrace:\n");
            */
            builder.append(ExceptionHelper.getDescriptionWithStackTrace(exception));
        }

        addToReport(builder);

        builder.append("\n\nLog:\n");
        if (null == logFileName || logFileName.isEmpty()) {
            builder.append("no log\n");
        }
        else {
            FineFileReader reader = new FineFileReader(logFileName);
            String line;
            int count = 0;
            while (null != (line = reader.readNextLine())) {
                builder.append(line + "\n");
                ++count;
                if (lastWrittenLineNumber > -1 && count >= lastWrittenLineNumber) {
                    break;
                }
            }
        }

        /* Den Benutzer nach dem Speicherort fragen: */
        String home = SystemTools.getHomeDirectory();
        FileFilter fileFilter = GuiTools.createTextFileFilter();
        String filename = GuiTools.saveFileAsWithTitle("Speichern des Berichts", getDialog(), home,
                fileFilter, createPureReportFilenameSuggestion());
        if (filename.isEmpty()) { // Abbruch des Benutzers
            return;
        }

        if (!filename.endsWith(".txt")) {
            filename = filename + ".txt";
        }

        /* Bericht speichern. */
        FineFileWriter writer = new FineFileWriter(filename);
        writer.write(builder.toString());
        writer.close();
    }

    private String createPureReportFilenameSuggestion() {
        String addon = createPureReportFilenameSuggestionAddon();
        if (!addon.isEmpty()) {
            addon = "_" + addon;
        }
        return Timestamp.fullTimestamp() + "_error_report" + addon + ".txt";
    }

    protected String createPureReportFilenameSuggestionAddon() {
        return "";
    }

    /**
     * Diese Methode kann überschrieben werden, um eigene Inhalte anzuzeigen.
     * Vergleiche GraphErrorDialog im Projekt graph.
     *
     * @param builder
     *            StringBuilder, an den eigene Inhalte angefügt werden können.
     */
    protected void addToReport(StringBuilder builder) {
        // hier passiert nichts, diese Methode kann jedoch überschrieben werden,
        // um eigene Inhalte anzuzeigen. Vergleiche GraphErrorDialog im Projekt
        // graph
    }

    /** Setzt Vorder- und Hintergrundfarbe einer Komponente. */
    @Override
    protected void setColors(Component component) {
        component.setBackground(backgroundColor);
        component.setForeground(foregroundColor);
    }

}
