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

/*
 * 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.util.List;

import de.duehl.basics.debug.DebugHelper;

import de.duehl.swing.ui.filter.dialog.FilterCombinationLine;
import de.duehl.swing.ui.filter.exceptions.FilterException;
import de.duehl.swing.ui.filter.method.FilterMethodFabricable;
import de.duehl.swing.ui.filter.method.combination.CombinationElement;
import de.duehl.swing.ui.filter.method.combination.CombinationElementFabric;
import de.duehl.swing.ui.filter.method.combination.CombinationElementList;
import de.duehl.swing.ui.filter.project.gateway.DescriptionToTypeTranslater;

/**
 * Diese Klasse erzeugt für den Filterkombinationsdialog 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.
 *
 * @version 1.01     2021-04-27
 * @author Christian Dühl
 */

public class RealFilterCreater<Data, Type> {

    private static final boolean VERBOSE = false;

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

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

    /** Objekt das zu einer eindeutigen Beschreibung den passenden Typen liefert. */
    private final DescriptionToTypeTranslater<Type> descriptionTranslater;

    /**
     * Konstruktor
     *
     * @param methodFabric
     *            Fabrik die je nach der übergebener Filter-Beschreibung eine entsprechende
     *            Filter-Methoden-Klasse herstellt.
     * @param lines
     *            GUI-Zeilen der Kombination.
     * @param descriptionTranslater
     *            Objekt das zu einer eindeutigen Beschreibung den passenden Typen liefert.
     */
    public RealFilterCreater(FilterMethodFabricable<Data, Type> methodFabric,
            List<FilterCombinationLine<Data, Type>> lines,
            DescriptionToTypeTranslater<Type> descriptionTranslater) {
        this.methodFabric = methodFabric;
        this.lines = lines;
        this.descriptionTranslater = descriptionTranslater;
    }

    /**
     * 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.
     * @return
     *
     * @throws FilterException
     *             Wird im Fehlerfall geworfen.
     */
    public CombinationElementList<Data> createRealFilter() {
        /*
         * Zuerst bauen wir eine rudimentäre element-Liste auf, bei der aber die zweistelligen
         * Operatoren noch zwischen ihren Operanden stehen und nicht davor:
         */
        CombinationElementList<Data> elements = createPreliminaryElementList();
        say("preliminary element List: " + elements);

        /*
         * Nun werden die Vereinigungen behandelt und richtig sortiert (denn diese haben Vorrang
         * vor den Schnitten). Dafür suchen wir immer von links nach der ersten unbehandelten
         * Vereinigung und korrigieren diese:
         */
        int operatorIndex = -1;
        boolean work = true;
        while (work) {
            /* Bestimme erste noch nicht bearbeitete Vereinigung: */
            ++operatorIndex;
            while (!elements.get(operatorIndex).isUnion()) {
                ++operatorIndex;
                if (operatorIndex >= elements.size()) {
                    work = false;
                    break;
                }
            }
            if (work) {
                /*
                 * Nun haben wir die von links aus erste noch nicht bearbeitete Vereinigung
                 * gefunden, und zwar am Index operatorIndex. Nun untersuchen wir das, was links
                 * davor steht:
                 */
                int moveToIndex = getStartPositionOfLeftOperand(elements, operatorIndex);

                /* ... und verschieben die Vereinigung davor: */
                CombinationElement<Data> union = elements.remove(operatorIndex);
                elements.add(moveToIndex, union);
                say("Verschiebe Vereinigung von " + operatorIndex + " nach " + moveToIndex + ".");
            }
        }

        say("preliminary element List nach Verschieben der Vereinigungen: " + elements);

        /*
         * Nun werden die Schnitte behandelt und richtig sortiert (hierbei ist zu berücksichtigen,
         * dass die Vereinigungen schon an den richtigen Stellen sind und dass diese Vorrang haben,
         * was bedeutet, dass man eventuell weiter nach links suchen muss). Dafür suchen wir immer
         * von der Position hinter dem ersten Operand nach einem weiterem und einer Vereinigung
         * davor (oder noch einem Operand und zwei Vereinigungen...)
         */
        operatorIndex = -1;
        work = true;
        while (work) {
            /* Bestimme erste noch nicht bearbeitete Schnittbildung: */
            ++operatorIndex;
            while (!elements.get(operatorIndex).isIntersection()) {
                ++operatorIndex;
                if (operatorIndex >= elements.size()) {
                    work = false;
                    break;
                }
            }
            if (work) {
                /*
                 * Nun haben wir die von links aus erste noch nicht bearbeitete Schnittbildung
                 * gefunden, und zwar am Index operatorIndex. Nun untersuchen wir das, was links
                 * davor steht und verschieben die Schnittbildung davor:
                 */
                int moveToIndex = getStartPositionOfLeftOperand(elements, operatorIndex);

                /*
                 * Normalerweise wäre moveToIndex der Zielindex, nun müssen wir aber weiter nach
                 * links schauen, ob dort eine Vereinigung, deren Ergebnis der linke Operand der
                 * Schnittbildung ist.
                 *
                 * Man muss dafür die Anzahl der Operanden links von der moveToIndex-Position
                 * bestimmen und schauen, ob es links davon genauso viele Vereinigungen gibt. Ist
                 * dies der Fall, wird der moveToIndex auf die linkeste dieser Vereinigungen
                 * gesetzt:
                 */
                say("Würde Schnittbildung von " + operatorIndex + " nach " + moveToIndex
                        + " verschieben.");
                moveToIndex = jumpOverNOperandsAndUnionsToLeft(elements, moveToIndex);

                /* Schnittoperator verschieben: */
                CombinationElement<Data> union = elements.remove(operatorIndex);
                elements.add(moveToIndex, union);
                say("Verschiebe Schnittbildung von " + operatorIndex + " nach " + moveToIndex + ".");
            }
        }

        return elements;
    }

    /**
     * Es wird eine rudimentäre element-Liste aus CombinationElement-Objekten aufgebaut, bei der
     * aber die zweistelligen Operatoren noch zwischen ihren beiden Operanden stehen und nicht
     * davor.
     *
     * @return Aufgebaute Liste von CombinationElement-Objekten.
     * @throws FilterException
     *             Bei Fehlern wird diese Ausnahme geworfen.
     */
    private CombinationElementList<Data> createPreliminaryElementList() {
        /* Dies wird die Liste von Elementen, die wir aufbauen: */
        CombinationElementList<Data> elements = new CombinationElementList<Data>();

        /* Die Fabrik brauchen wir um die passenden CombinationElemente herzustellen: */
        CombinationElementFabric<Data, Type> fabric = new CombinationElementFabric<>(methodFabric);


        say("---------------------------");
        for (FilterCombinationLine<Data, Type> line : lines) {
            if (line.isMethodLine()) {
                String description = line.getMethodDescription();
                if (null == description) {
                    throw new FilterException("Bekomme zu Methodenzeile keinen Typ");
                }
                Type type = descriptionTranslater.getTypeForDescription(description);
                CombinationElement<Data> element = fabric.createMethod(type);
                elements.add(element);
                say("Füge type " + type + " hinzu.");
            }
            else if (line.isNegatedMethodLine()) {
                CombinationElement<Data> not = fabric.createNegation();
                elements.add(not);
                say("Füge 'nicht' hinzu.");

                String description = line.getMethodDescription();
                if (null == description) {
                    throw new FilterException("Bekomme zu Methodenzeile keine Beschreibung.");
                }
                Type type = descriptionTranslater.getTypeForDescription(description);
                CombinationElement<Data> element = fabric.createMethod(type);
                elements.add(element);
                say("Füge type " + type + " hinzu.");
            }
            else if (line.isParamisedMethodLine()) {
                String description = line.getMethodDescription();
                if (null == description) {
                    throw new FilterException("Bekomme zu parametrisierter Methodenzeile keine "
                            + "Bescheibung.");
                }
                Type type = descriptionTranslater.getTypeForDescription(description);
                String parameter = line.getParameter();
                if ("".equals(parameter)) {
                    throw new FilterException("Bekomme zu parametrisierter Methodenzeile leeren "
                            + "Parameter");
                }
                CombinationElement<Data> element = fabric.createInputMethod(type, parameter);
                elements.add(element);
                say("Füge type " + type + " mit Parameter '" + parameter + "' hinzu.");
            }
            else if (line.isParamisedNegatedMethodLine()) {
                CombinationElement<Data> not = fabric.createNegation();
                elements.add(not);
                say("Füge 'nicht' hinzu.");

                String description = line.getMethodDescription();
                if (null == description) {
                    throw new FilterException("Bekomme zu parametrisierter Methodenzeile keine "
                            + "Beschreibung.");
                }
                Type type = descriptionTranslater.getTypeForDescription(description);
                String parameter = line.getParameter();
                if ("".equals(parameter)) {
                    throw new FilterException("Bekomme zu parametrisierter Methodenzeile leeren "
                            + "Parameter");
                }
                CombinationElement<Data> element = fabric.createInputMethod(type, parameter);
                elements.add(element);
                say("Füge type " + type + " mit Parameter '" + parameter + "' hinzu.");
            }
            else if (line.isBraceOpenLine()) {
                CombinationElement<Data> element = fabric.createOpeningBrace();
                elements.add(element);
                say("Füge '(' hinzu.");
            }
            else if (line.isNegatedBraceOpenLine()) {
                CombinationElement<Data> not = fabric.createNegation();
                elements.add(not);
                say("Füge 'nicht' hinzu.");

                CombinationElement<Data> element = fabric.createOpeningBrace();
                elements.add(element);
                say("Füge '(' hinzu.");
            }
            else if (line.isBraceCloseLine()) {
                CombinationElement<Data> element = fabric.createClosingBrace();
                elements.add(element);
                say("Füge ')' hinzu.");
            }
            else if (line.isIntersectionLine()) {
                CombinationElement<Data> element = fabric.createIntersection();
                elements.add(element);
                say("Füge 'geschnitten mit' hinzu.");
            }
            else if (line.isUnionLine()) {
                CombinationElement<Data> element = fabric.createUnion();
                elements.add(element);
                say("Füge 'vereinigt mit' hinzu.");
            }
        }
        say("---------------------------");

        return elements;
    }

    /**
     * Untersucht den Operand links von der gegebenen Operatorposition und gibt dessen
     * Startposition zurück.
     *
     * @param elements
     *            Liste von CombinationElement-Objekten
     * @param operatorIndex
     *            Index des Operators, dessen linker Operand untersucht werden soll.
     * @return Index des Anfangs des linken Operands oder den Ausgangsindex, falls es eine solche
     *         Struktur links nicht gibt.
     * @throws FilterException
     *             Wird im Falle eines Fehlers geworfen.
     */
    private int getStartPositionOfLeftOperand(CombinationElementList<Data> elements,
            int operatorIndex) {
        int moveToIndex = operatorIndex;

        if (operatorIndex == 0) {
            throw new FilterException("Zweistelliger Operator an erster Stelle gefunden.");
        }

        /* Nun suchen wir den Index, zu dem die Vereinigung verschoben werden soll: */
        int leftElementIndex = operatorIndex - 1;
        CombinationElement<Data> leftElement = elements.get(leftElementIndex);
        if (leftElement.isMethod()) {
            moveToIndex = leftElementIndex;
            /* Vielleicht ist es eine negierte Methode? */
            if (moveToIndex > 0 && elements.get(moveToIndex - 1).isNegation()) {
                --moveToIndex;
            }
        }
        else if (leftElement.isClosingBrace()) {
            moveToIndex = findMatchingLeftBrace(elements, leftElementIndex);
            /* Vielleicht ist es eine negierte Klammer? */
            if (moveToIndex > 0 && elements.get(moveToIndex - 1).isNegation()) {
                --moveToIndex;
            }
        }

        return moveToIndex;
    }

    /**
     * Sucht nach der passenden linken Klammer.
     *
     * @param elements
     *            Liste von CombinationElement-Objekten
     * @param rightBraceIndex
     *            Index der rechten Klammer
     * @return Index der passenden linken Klammer oder -1 wenn keine gefunden
     *         wurde.
     */
    private int findMatchingLeftBrace(CombinationElementList<Data> elements, int rightBraceIndex) {
        int leftBraceIndex = rightBraceIndex - 1;
        int countBraces = 1;

        while (leftBraceIndex >= 0) {
            CombinationElement<Data> element = elements.get(leftBraceIndex);
            if (element.isClosingBrace()) {
                ++countBraces;
            }
            else if (element.isOpeningBrace()) {
                --countBraces;
                if (0 == countBraces) {
                    break;
                }
            }
            --leftBraceIndex;
        }

        return leftBraceIndex;
    }

    /**
     * Versucht über die gleiche Anzahl an Operanden links von der gegebenen moveToIndex-Position
     * und die gleiche Anzahl von Vereinigungen zu springen und gibt den Index der am weitesten
     * links liegenden Vereinigung zurück.
     *
     * @param elements
     *            Liste von CombinationElement-Objekten
     * @param startIndex
     *            Start Index : suche links neben diesem
     * @return Index der am weitesten links liegenden Vereinigung oder den Ausgangsindex, falls es
     *         eine solche Struktur links nicht gibt.
     * @throws FilterException
     *             Wird im Falle eines Fehlers geworfen.
     */
    private int jumpOverNOperandsAndUnionsToLeft(CombinationElementList<Data> elements,
            int startIndex) {
        int moveToindex = startIndex;

        /* Über Operanden nach links springen, solange welche da sind: */
        boolean goLeft = moveToindex > 0;
        int countOperands = 0;
        while (goLeft) {
            int index = getStartPositionOfLeftOperand(elements, moveToindex);
            if (index == moveToindex) {
                goLeft = false;
            }
            else {
                ++countOperands;
                moveToindex = index;
            }
        }

        /*
         * Nun wird geschaut, ob vor diesen countOperands Operatoren auch genauso viele
         * Vereinigungen stehen:
         */
        boolean foundUnions = true;
        for (int i=1; i<=countOperands; ++i) {
            int index = moveToindex - i;
            if (index < 0) {
                foundUnions = false;
                break;
            }
        }

        /*
         * Im Erfolgsfall setzen wir den Index auf die erste Vereinigung, sonst zurück auf den
         * Ausgangsindex:
         */
        if (foundUnions) {
            moveToindex -= countOperands;
        }
        else {
            moveToindex = startIndex;
        }

        return moveToindex;
    }

    private void say(String message) {
        if (VERBOSE) {
            DebugHelper.sayWithClassAndMethod(message);
        }
    }

}
