package de.duehl.swing.ui.filter.dialog;

/*
 * Copyright 2021 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.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Image;
import java.awt.Point;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JScrollPane;

import de.duehl.basics.io.Charset;
import de.duehl.basics.io.FileHelper;
import de.duehl.basics.io.FineFileWriter;
import de.duehl.basics.io.textfile.FullNormalTextFileReader;
import de.duehl.basics.io.textfile.FullTextFileReader;
import de.duehl.basics.logic.ErrorHandler;
import de.duehl.swing.ui.GuiTools;
import de.duehl.swing.ui.dialogs.base.ModalDialogBase;
import de.duehl.swing.ui.filefilter.FilterFileFilter;
import de.duehl.swing.ui.filter.dialog.creation.RealFilterCreater;
import de.duehl.swing.ui.filter.exceptions.FilterException;
import de.duehl.swing.ui.filter.method.FilterMethodFabricable;
import de.duehl.swing.ui.filter.project.gateway.FilterGateway;
import de.duehl.swing.ui.layout.VerticalLayout;

/**
 * Diese Klasse erzeugt die grafische Oberfläche des Filterkombinationsdialoges.
 *
 * @version 1.01     2021-05-05
 * @author Christian Dühl
 */

public class FilterCombinationDialog<Data, Type> extends ModalDialogBase
        implements FilterCombinationLineHandler<Data, Type>  {

    private static final Dimension DIALOG_DIMENSION = new Dimension(850, 550);

    private static final String DEFAULT_SAVE_FILTER_FILENAME = "DEFAULT_SAVE_FILTER.filter";

    /** Objekt das eindeutige Beschreibungen der Methoden zum Filtern liefert. */
    private final FilterGateway<Data, Type> gateway;

    /**
     * Fabrik die je nach der übergebener Filter-Beschreibung eine entsprechende
     * Filter-Methoden-Klasse herstellt.
     */
    private final FilterMethodFabricable<Data, Type> methodFabric;

    /** Pfad zum Laden und Speichern von Filtern. */
    private final String filterPath;

    /** Objekt zur passenden Behandlung von Fehlern. */
    private final ErrorHandler error;

    private final String saveFileName;

    /** GUI-Zeilen der Kombination. */
    private final List<FilterCombinationLine<Data, Type>> lines;

    /** Panel mit den Zeilen des Filters. */
    private final JPanel combinationPanel;

    /** Gibt an, ob beim Anwenden von apply ein Fehler auftrat. */
    private boolean errorOccured;

    /**
     * Der Konstruktor.
     *
     * @param parentLocation
     *            Position des Rahmens der Oberfläche, vor der dieser Dialog erzeugt wird.
     * @param programImage
     *            Icon für das Programm.
     * @param filterMethods
     *            Objekt das eindeutige Beschreibungen der Methoden zum Filtern liefert.
     * @param filterPath
     *            Pfad zum Laden und Speichern von Filtern.
     * @param methodFabric
     *            Fabrik die je nach der übergebener Filter-Beschreibung eine entsprechende
     *            Filter-Methoden-Klasse herstellt.
     * @param error
     *            Objekt zur passenden Behandlung von Fehlern.
     */
    public FilterCombinationDialog(Point parentLocation, Image programImage,
            FilterGateway<Data, Type> filterMethods,
            FilterMethodFabricable<Data, Type> methodFabric, String filterPath,
            ErrorHandler error) {
        super(parentLocation, programImage, "Filterkombinationen", DIALOG_DIMENSION);
        this.gateway = filterMethods;
        this.methodFabric = methodFabric;
        this.filterPath = filterPath;
        this.error = error;

        saveFileName = FileHelper.concatPathes(filterPath, DEFAULT_SAVE_FILTER_FILENAME);
        lines = new ArrayList<>();

        combinationPanel = new JPanel();

        initElements();
        fillDialog();
        //setLocation(100,  100);

        load(saveFileName);
    }

    private void initElements() {
        combinationPanel.setLayout(new VerticalLayout(5, VerticalLayout.LEFT));
    }

    /** Baut die Gui auf. */
    @Override
    protected void populateDialog() {
        add(createBuildPart(), BorderLayout.NORTH);
        add(new JScrollPane(combinationPanel), BorderLayout.CENTER);
        add(createButtonPart(), BorderLayout.SOUTH);
    }

    /** Erstellt den oberen Bastel-Bereich. */
    private Component createBuildPart() {
        JPanel panel = new JPanel(new BorderLayout());

        panel.add(createLeftBuildPanel(), BorderLayout.WEST);

        return panel;
    }

    private Component createLeftBuildPanel() {
        JPanel panel = new JPanel(new FlowLayout());

        panel.add(createNewLineButton());
        panel.add(createClearAllButton());

        return panel;
    }

    private Component createNewLineButton() {
        JButton button  = new JButton("neue Zeile");
        button.addActionListener(e -> newLine());
        return button;
    }

    private void newLine() {
        FilterCombinationLine<Data, Type> newLine = new FilterCombinationLine<>(combinationPanel,
                lines.size(), gateway, (FilterCombinationLineHandler<Data, Type>) this);
        validate(); // für den Scrollbalken bei vielen Zeilen!
        lines.add(newLine);
    }

    private Component createClearAllButton() {
        JButton button  = new JButton("alles löschen");
        button.addActionListener(e -> clearAll());
        return button;
    }

    private void clearAll() {
        combinationPanel.removeAll();
        lines.clear();
        combinationPanel.validate();
        validate(); // Scrollbar entfernen, falls vorhanden
        repaint(); // wird auch gebraucht
    }

    /** Erstellt die untere Button-Leiste. */
    private Component createButtonPart() {
        JPanel panel = new JPanel(new BorderLayout());

        panel.add(createRightButtonPart(), BorderLayout.EAST);
        panel.add(createLeftButtonPart(), BorderLayout.WEST);

        return panel;
    }

    private JPanel createLeftButtonPart() {
        JPanel leftPanel = new JPanel(new FlowLayout());
        leftPanel.add(createLoadButton());
        leftPanel.add(createSaveButton());
        return leftPanel;
    }

    private JButton createLoadButton() {
        JButton button  = new JButton("Laden");
        button.addActionListener(e -> load());
        return button;
    }

    private JButton createSaveButton() {
        JButton button  = new JButton("Speichern");
        button.addActionListener(e -> save());
        return button;
    }

    private JPanel createRightButtonPart() {
        JPanel rightPanel = new JPanel(new FlowLayout());
        rightPanel.add(createCancelButton());
        rightPanel.add(createApplyButton());
        return rightPanel;
    }

    private JButton createCancelButton() {
        JButton button  = new JButton("Abbrechen");
        button.addActionListener(e -> closeDialog());
        return button;
    }

    private JButton createApplyButton() {
        JButton button  = new JButton("FIlter erzeugen");
        button.addActionListener(e -> applyButtonPressed());
        return button;
    }

    /** Reagiert auf den gedrückten Apply-Button. */
    private void applyButtonPressed() {
        GuiTools.waitcursorImmediatelyOn(getDialog());
        // Besser in Extra Thread, geht aber auch so
        boolean success = checkFilter(); // das ist im Gui-Thread ok.
        if (success) {
            try {
                tryToApply();
            }
            catch (FilterException exception) {
                handleExceptionWhileApplying(exception);
            }
        }
        GuiTools.waitcursorOff(getDialog());
    }

    private void tryToApply() {
        errorOccured = false;
        createRealFilter();
        if (errorOccured) {
            requestFocus();
        }
        else {
            save(saveFileName);

            if (gateway.isFilterCombinedMethods()) {
                closeDialog();
            }
            else {
                requestFocus();
            }
        }
    }

    private void createRealFilter() {
        try {
            tryToCreateRealFilter();
        }
        catch (Exception exeption) {
            errorOccured = true;
            error.error("Fehler beim Erzeugen des echten Filters.", exeption);
        }
    }

    private void handleExceptionWhileApplying(FilterException exception) {
        StackTraceElement[] traces = exception.getStackTrace();
        StringBuilder traceString = new StringBuilder();
        int c = 0;
        for (StackTraceElement t : traces) {
            if (++c > 5) {
                traceString.append("...\n");
                break;
            }
            traceString.append(t.getFileName() + " - " + t.getClassName() + " - "
                    + t.getMethodName() + " - " + t.getLineNumber() + "\n");
        }
        error.warning("Bei der Erzeugung des Filters trat ein Fehler auf:\n\n"
                + exception.getMessage() + "\n\nStacktrace: " + traceString.toString());
    }

    /** Testet die Daten des Benutzers so weit dies hier geht. */
    private boolean checkFilter() {
        if (lines.size() < 1) {
            error.warning("Der Filte ist leer!");
            return false;
        }

        int countBraceOpen = 0;
        int countBraceClose = 0;

        for (FilterCombinationLine<Data, Type> line : lines) {
            if (line.isInStartPhase()) {
                error.warning("Mindestens eine Zeile ist noch im Start-Zustand!");
                return false;
            }
            else if (line.isMethodLine() || line.isNegatedMethodLine()) {
                String selection = line.getMethodComboBoxSelection();
                if (selection.equals(FilterCombinationLine.BITTE_WAEHLEN)) {
                    error.warning("Bitte paramterlose Methode wählen!");
                    return false;
                }
            }
            else if (line.isParamisedMethodLine()
                    || line.isParamisedNegatedMethodLine()) {
                String selection = line.getMethodComboBoxSelection();
                String parameter = line.getParameter();
                if (selection.equals(FilterCombinationLine.BITTE_WAEHLEN)
                         || parameter.isEmpty()) {
                    error.warning("Bitte paramtrisierte Methode wählen!");
                    return false;
                }
            }
            else if (line.isBraceOpenLine() || line.isNegatedBraceOpenLine()) {
                ++countBraceOpen;
            }
            else if (line.isBraceCloseLine()) {
                ++countBraceClose;
            }
            else if (line.isIntersectionLine() || line.isUnionLine()) {
                ; // ist in Ordnung
            }
            else {
                error.warning("Unbekanntes FilterCombinationLine-Objekt: " + line.getTypeAsString());
                return false;
            }
        }

        if (countBraceClose != countBraceOpen) {
            error.warning("Unterschiedliche Anzahl öffnender und schließender Klammern.");
            return false;
        }

        return true;
    }

    /**
     * Erzeugt aus der Liste 'lines' der FilterCombinationLine-Objekte eine Liste von
     * CombinationElement-Objekten. Dabei wird die Umstellung der zweistelligen Operatoren
     * Schnittmengenbildung und Vereinigungsbildung von Operatoren zwischen den beiden Operanden in
     * der Ausgangsliste zu Operatoren vor zwei Operanden in der Zielliste umgestellt.
     *
     * Aus dieser Liste von CombinationElement-Objekten wird dann ein konkreter kombinierter Filter
     * erstellt, welcher auf die im Hauptprogramm angezeigte Datei angewendet wird.
     *
     * @throws FilterException
     *             Wird im Fehlerfall geworfen.
     */
    private void tryToCreateRealFilter() {
        RealFilterCreater<Data, Type> creater =
                new RealFilterCreater<>(methodFabric, lines, gateway);
        gateway.setRealFilter(creater.createRealFilter());
    }

    /** Lädt einen Filter und fragt den Benutzer nach dem Dateinamen. */
    private void load() {
        String filename = GuiTools.openFile(getDialog(), filterPath, new FilterFileFilter());

        if (!filename.isEmpty()) {
            load(filename);
        }
    }

    /**
     * Lädt einen Filter unter dem angegebenen Namen.
     *
     * @param filename
     *            Dateiname
     */
    private void load(String filename) {
        /* Alle Zeilen aus der Menge der Zeilen entfernen: */
        lines.clear();

        /* Alle Gui-Zeilen entfernen: */
        combinationPanel.removeAll();

        /* Datei einlesen: */
        if (FileHelper.isFile(filename)) {
            loadExistingFile(filename);
        }

        /* Neu zeichnen: */
        combinationPanel.validate();
        combinationPanel.repaint();
        validate(); // für den Scrollbalken bei vielen Zeilen!
        repaint();
    }

    private void loadExistingFile(String filename) {
        try {
            tryToLloadExistingFile(filename);
        }
        catch (Exception exception) {
            error.error("Fehler beim Laden des Filters aus der Datei\n\t" + filename, exception);
        }
    }

    private void tryToLloadExistingFile(String filename) {
        FullTextFileReader reader = new FullNormalTextFileReader(filename, Charset.ISO_8859_1);
        reader.setLineDescription("Zeile einer Filterdatei");
        reader.setProgressTextStart("Einlesen der Filterdatei");
        //reader.skipFirstLine();
        reader.beQuiet();
        reader.read(line -> analyseLine(line, reader.getLineNumber()));
    }

    private void analyseLine(String line, int lineNumber) {
        if (line.equals("")) {
            GuiTools.informUser(getDialog(), "Information",
                    "Skipping empty line " + lineNumber);
        }
        else {
            FilterCombinationLine<Data, Type> newLine = new FilterCombinationLine<>(
                    combinationPanel, lines.size(), gateway,
                    (FilterCombinationLineHandler<Data, Type>) this, line);
            lines.add(newLine);
        }
    }

    /** Speichert den Filter ab und fragt den Benutzer nach dem Dateinamen. */
    private void save() {
        String filename = GuiTools.saveFileAs(getDialog(), filterPath, new FilterFileFilter());

        if (!filename.isEmpty()) {
            if (!filename.endsWith(".filter")) {
                filename += ".filter";
            }
            save(filename);
        }
    }

    /**
     * Speichert den Filter unter dem angegebenen Namen ab.
     *
     * @param fileName
     *            Dateiname
     */
    private void save(String fileName) {
        try {
            tryToSave(fileName);
        }
        catch (Exception exeption) {
            error.error("Fehler beim Speichern des Filters unter der Datei\n\t" + fileName,
                    exeption);
        }
    }

    private void tryToSave(String fileName) {
        FineFileWriter writer = new FineFileWriter(fileName);

        for (FilterCombinationLine<Data, Type> line : lines) {
            line.write(writer);
        }

        writer.close();
    }

    /** Entfernt die angegebene Zeile aus der Liste und aus der Oberfläche: */
    @Override
    public void removeLine(FilterCombinationLine<Data, Type> line) {
        /* Zeilennummern in den Zeilen darunter um eins verkleinern: */
        int lineNumber = line.getLineNumber();
        for (int lowerIndex = lineNumber + 1; lowerIndex < lines.size(); ++lowerIndex) {
            FilterCombinationLine<Data, Type> lowerLine = lines.get(lowerIndex);
            lowerLine.setLineNumber(lowerLine.getLineNumber() - 1);
        }

        /* Zeile aus der Menge der Zeilen entfernen: */
        lines.remove(lineNumber);

        /* Die veränderten Zeilen anzeigen: */
        showAllLinesAgainInCombinationPanel();
    }

    /** Fügt eine Zeile unter der angegebene Zeile hinzu: */
    @Override
    public void addLine(FilterCombinationLine<Data, Type> line) {
        /* Zeilennummern in den Zeilen darunter um eins vergrößern: */
        int lineNumber = line.getLineNumber();
        for (int lowerIndex = lineNumber + 1; lowerIndex < lines.size(); ++lowerIndex) {
            FilterCombinationLine<Data, Type> lowerLine = lines.get(lowerIndex);
            lowerLine.setLineNumber(lowerLine.getLineNumber() + 1);
        }

        /* Neue Zeile unter der aktuellen erzeugen und eintragen: */
        FilterCombinationLine<Data, Type> theNewLine = new FilterCombinationLine<>(combinationPanel,
                lineNumber + 1, gateway, (FilterCombinationLineHandler<Data, Type>) this);
        lines.add(lineNumber + 1, theNewLine);

        /* Die veränderten Zeilen anzeigen: */
        showAllLinesAgainInCombinationPanel();
    }

    private void showAllLinesAgainInCombinationPanel() {
        /* Alle Gui-Zeilen entfernen: */
        combinationPanel.removeAll();

        /* Restliche Zeilen neu eintragen: */
        for (FilterCombinationLine<Data, Type> newLine : lines) {
            combinationPanel.add(newLine.getLinePanel());
            newLine.validate();
        }

        /* Änderungen anzeigen: */
        combinationPanel.validate();
        combinationPanel.repaint();
        validate(); // für den Scrollbalken bei vielen Zeilen!
    }

}
