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

/*
 * Copyright 2020 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.ArrayList;
import java.util.Collections;
import java.util.List;

import de.duehl.swing.ui.filter.exceptions.FilterException;
import de.duehl.swing.ui.filter.method.combination.CombinationElement;
import de.duehl.swing.ui.filter.method.combination.CombinationElementList;

/**
 * Diese Klasse repräsentiert die Filtermethode 'Datensätze mit Filtermethodenkombination'
 * (COMBINED_METHODS).
 *
 * @version 1.01     2020-11-05
 * @author Christian Dühl
 */

public class MethodCombination<Data> implements Method<Data> {

    /**
     * Diese Klasse enthält Elemente beim Übergang von einer Liste von CombinationElement-Objekten
     * zu einer einzigen Liste von Treffern. Die Elemente können entweder Listen sein, oder
     * CombinationElement-Objekte.
     */
    private class ElementOrList {
        private final boolean isList;
        private final List<Integer> list;
        private final CombinationElement<Data> element;

        public ElementOrList(List<Integer> list) {
            this.isList  = true;
            this.list    = list;
            this.element = null;
        }

        public ElementOrList(CombinationElement<Data> element) {
            this.isList  = false;
            this.list    = null;
            this.element = element;
        }

        public boolean isList() {
            return isList;
        }

        public List<Integer> getList() throws FilterException {
            if (!isList) {
                throw new FilterException("ElementOrList enthält ein "
                                + "Element, man will aber die Liste.");
            }
            return list;
        }

        public CombinationElement<Data> getElement() throws FilterException {
            if (isList) {
                throw new FilterException("ElementOrList enthält eine "
                        + "Liste, man will aber das Element.");
            }
            return element;
        }
    }

    /** Liste mit den Kombinationselementen. */
    private CombinationElementList<Data> elements;

    /**
     * Konstruktor, hinterlegt die Kombinationselemente.
     *
     * @param elements
     *            Kombinationselemente.
     * @throws FilterException
     *             Bei Fehlern wird diese Ausnahme geworfen.
     */
    public MethodCombination(CombinationElementList<Data> elements) {
        this.elements = elements;

        if (0 == elements.size()) {
            throw new FilterException("Leere Listen sind nicht erlaubt!");
        }
    }

    /**
     * Diese Methode baut eine Liste mit Indices (Start bei 1) der Elemente aus den Datensätzen,
     * die der Filtermethode entsprechen.
     *
     * @param datasets
     *            Datensätze
     * @throws FilterException
     *             Bei Fehlern wird diese Ausnahme geworfen.
     */
    @Override
    public List<Integer> buildFilter(List<Data> datasets) {
        /*
         * Zunächst erzeugen wir aus den Elementen eine Liste von Listen oder Elementen, dabei
         * werden alle Methoden schon einmal in Listen umgewandelt:
         */
        List<ElementOrList> elementsOrLists = new ArrayList<>();
        for (CombinationElement<Data> element : elements) {
            ElementOrList elementOrList;
            if (element.isMethod()) {
                Method<Data> method = element.getMethod();
                List<Integer> list = createListByMethod(method, datasets);
                elementOrList = new ElementOrList(list);
            }
            else {
                elementOrList = new ElementOrList(element);
            }
            elementsOrLists.add(elementOrList);
        }

        /* Dann bearbeiten wir diese Liste: */
        List<Integer> total = new ArrayList<>();
        for (int i = 0; i < datasets.size(); ++i) {
            total.add(i + 1);
        }
        workOnElementsOrLists(elementsOrLists, total);

        /* Fehlerfall nicht exakt ein Element übrig: */
        if (elementsOrLists.size() != 1) {
            throw new FilterException("Nach rekursiver Bearbeitung ist nicht "
                    + "genau ein ElementOrList-Objekt übriggeblieben.");
        }

        /* Fehlerfall ein Element übrig, aber das ist keine Liste: */
        if (!elementsOrLists.get(0).isList()) {
            throw new FilterException("Nach rekursiver Bearbeitung ist genau "
                    + "ein ElementOrList-Objekt\nübriggeblieben, aber dieses "
                    + "ist keine Liste!");
        }

        /* Wir geben die Liste der Indices zurück: */
        return elementsOrLists.get(0).getList();
    }

    /**
     * Diese Methode bearbeitet rekursiv die Liste von ElementOrList-Objekten, am Ende der
     * Bearbeitung bleibt nur ein einziges Objekt übrig. Dieses Objekt ist eine Liste, die fertige
     * Liste mit den Indices (Start bei 1).
     *
     * @param elementsOrLists
     *            Liste mit ElementOrList-Objekten.
     * @param total
     *            Liste mit allen Indices (beginnend bei 1)
     * @throws FilterException
     *             Bei Fehlern wird diese Ausnahme geworfen.
     */
    private void workOnElementsOrLists(List<ElementOrList> elementsOrLists, List<Integer> total) {
        /*
         * Suche nach erster öffnender Klammer, auf die eine schließende Klammer folgt, ohne dass
         * dazwischen eine weitere öffnende Klammer wäre:
         */
        int indexOpeningBrace = -1;
        int indexClosingBrace = -1;
        for (int i=0; i< elementsOrLists.size(); ++i) {
            ElementOrList elementOrList = elementsOrLists.get(i);
            if (!elementOrList.isList) {
                CombinationElement<Data> element = elementOrList.getElement();
                if (element.isOpeningBrace()) {
                    indexOpeningBrace = i;
                }
                else if (element.isClosingBrace()) {
                    if (-1 != indexOpeningBrace) {
                        indexClosingBrace = i;
                        break;
                    }
                }
            }
        }

        /* Fehlerfall öffnende ohne schließende Klammer: */
        if (-1 != indexOpeningBrace && -1 == indexClosingBrace) {
            throw new FilterException(
                    "Fehler im Aufbau der Methodenkombinierung, öffnende\n"
                    + "Klammer ohne passende schließende Klammer gefunden.");
        }

        /* Fehlerfall schließende ohne öffnende Klammer: */
        else if (-1 == indexOpeningBrace && -1 != indexClosingBrace) {
            throw new FilterException(
                    "Fehler im Aufbau der Methodenkombinierung, schließende\n"
                    + "Klammer ohne passende öffnende Klammer gefunden.");
            /* kann durch die Suchlogik nicht auftreten! */
        }

        /*
         * Wir haben ein Klammerpaar in elementsOrLists gefunden:
         */
        else if (-1 != indexOpeningBrace && -1 != indexClosingBrace) {
            if (indexClosingBrace == 1 + indexOpeningBrace) {
                throw new FilterException("Fehler im Aufbau der Methodenkombinierung, "
                        + "Klammerpaar\nohne Inhalt gefunden.");
            }

            /*
             * Klammerpaar gefunden. Nun wird der Inhalt der Klammer
             * zusammengebaut und eine neue Liste erzeugt:
             */
            List<ElementOrList> braceContents = new ArrayList<ElementOrList>();
            for (int i=indexOpeningBrace+1; i<indexClosingBrace; ++i) {
                braceContents.add(elementsOrLists.get(i));
            }
            List<Integer> braceContentsAsOneList = workOnElementsWithoutBraces(braceContents, total);

            /* Nun hüllen wir die Liste in ein ElementOrList ein: */
            ElementOrList braceContentsAsOneElementOrList = new ElementOrList(braceContentsAsOneList);
            /*
             * Und nun setzten wir die Liste elementsOrLists auf eine Liste mit dem Teil vor der
             * Klammer, der erzeugten Liste und dem Teil nach der Klammer:
             */
            List<ElementOrList> front = new ArrayList<ElementOrList>();
            for (int i=0; i<indexOpeningBrace; ++i) {
                front.add(elementsOrLists.get(i));
            }
            List<ElementOrList> back = new ArrayList<ElementOrList>();
            for (int i=indexClosingBrace+1; i<elementsOrLists.size(); ++i) {
                back.add(elementsOrLists.get(i));
            }
            elementsOrLists.clear();
            for (ElementOrList thing : front) {
                elementsOrLists.add(thing);
            }
            elementsOrLists.add(braceContentsAsOneElementOrList);
            for (ElementOrList thing : back) {
                elementsOrLists.add(thing);
            }

            /* Und jetzt bearbeiten wir die neue Liste weiter: */
            workOnElementsOrLists(elementsOrLists, total);
        }

        /*
         * Anderenfalls ist elementsOrLists klammerfrei, also erzeugen wir aus der Gesamtmenge eine
         * Liste:
         */
        else {
            List<Integer> allAsOneList = workOnElementsWithoutBraces(elementsOrLists, total);
            /* Nun hüllen wir die Liste in ein ElementOrList ein: */
            ElementOrList allAsOneElementOrList = new ElementOrList(allAsOneList);
            /*
             * Und nun setzten wir die Liste elementsOrLists auf eine Liste mit nur einem Element,
             * nämlich allAsOneElementOrList:
             */
            elementsOrLists.clear();
            elementsOrLists.add(allAsOneElementOrList);
        }
    }

    /**
     * Diese Methode bearbeitet eine klammerfreie ElementOrList-Liste und erzeugt daraus eine Liste
     * mit den Indices (beginnend bei 1).
     *
     * @param bracelessContents
     *            klammerfreie Liste
     * @param total
     *            Liste mit allen Indices (beginnend bei 1)
     * @return Liste mit Indices
     * @throws FilterException
     *             Bei Fehlern wird diese Ausnahme geworfen.
     */
    private List<Integer> workOnElementsWithoutBraces(List<ElementOrList> bracelessContents,
            List<Integer> total) {
        /*
         * Was wir noch an Elementen haben:
         *  - Listen          - nullstellig
         *  - Negation        - einstellig
         *  - Schnittbildung  - zweistellig
         *  - Vereinigung     - zweistellig
         */

        while (1 < bracelessContents.size()) {
            /* Wir suchen den letzten Operator: */
            int operatorIndex = bracelessContents.size() - 1;
            while (operatorIndex >= 0 && bracelessContents.get(operatorIndex).isList()) {
                --operatorIndex;
            }
            /*
             * Wenn keiner gefunden wurde, darf die Liste nur ein Element haben, dann aber wären
             * wir nicht im Rumpf dieser while-Schleife:
             */
            if (operatorIndex < 0) {
                throw new FilterException("Liste mit mehr als einem Element,"
                        + " aber kein Operator gefunden.");
            }
            /* Wenn das letzte Element ein Operator ist, liegt ein Fehler vor: */
            if (operatorIndex == bracelessContents.size() - 1) {
                throw new FilterException("Letztes Element ist ein Operator.");
            }
            CombinationElement<Data> operator = bracelessContents.get(operatorIndex).getElement();
            if (operator.isIntersection() || operator.isUnion()) {
                if (operatorIndex + 2 >= bracelessContents.size()) {
                    throw new FilterException("Zu wenige Parameter für eine "
                            + "zweistellige Operation.");
                }
                /*
                 * Nun wissen wir, dass an den Stellen operatorIndex+1 und operatorIndex+2 jeweils
                 * Listen liegen.
                 */
                ElementOrList eol1 = bracelessContents.get(operatorIndex + 1);
                ElementOrList eol2 = bracelessContents.get(operatorIndex + 2);
                List<Integer> list1 = eol1.getList();
                List<Integer> list2 = eol2.getList();
                /* Nun bilden wir den Durchschnitt oder die Vereinigung: */
                List<Integer> operatedList;
                if (operator.isIntersection()) {
                    operatedList = intersectLists(list1, list2);
                }
                else {
                    operatedList = uniteLists(list1, list2);
                }
                /*
                 * Nun werfen wir die beiden alten Listen raus aus bracelessContents:
                 */
                bracelessContents.remove(operatorIndex + 2);
                bracelessContents.remove(operatorIndex + 1);
                /*
                 * Und ersetzen den Operator durch die Ergebnisliste, welche wir
                 * in ein ElementOrList-Objekt einhüllen:
                 */
                ElementOrList operatedElement = new ElementOrList(operatedList);
                bracelessContents.set(operatorIndex, operatedElement);
            }
            else if (operator.isNegation()) {
                /*
                 * Wir wissen, dass an der Stellen operatorIndex+1 eine Liste liegt.
                 */
                ElementOrList eol = bracelessContents.get(operatorIndex + 1);
                List<Integer> list = eol.getList();
                /* Nun bilden wir die Negation: */
                List<Integer> negatedList = negateList(list, total);
                /* Nun werfen wir die alte Liste raus aus bracelessContents: */
                bracelessContents.remove(operatorIndex + 1);
                /*
                 * Und ersetzen den Operator durch die Ergebnisliste, welche wir in ein
                 * ElementOrList-Objekt einhüllen:
                 */
                ElementOrList operatedElement = new ElementOrList(negatedList);
                bracelessContents.set(operatorIndex, operatedElement);
           }
           else {
               throw new FilterException("Unbekannter Operator.");
           }
        }

        /* Nun hat die Liste nur noch ein Element. Wir geben die Liste darin zurück: */
        ElementOrList thing = bracelessContents.get(0);

        /* Fehlerfall ein Element übrig, aber das ist keine Liste: */
        if (!thing.isList()) {
            throw new FilterException("Bei rekursiver Bearbeitung ist genau "
                    + "ein ElementOrList-Objekt\nübriggeblieben, aber "
                    + "dieses ist keine Liste!");
        }

        return thing.getList();
    }

    /**
     * Diese Methode bildet zu einer Liste mit Indices die Komplementärmenge. Dazu wird natürlich
     * die Gesamtmenge gebraucht.
     *
     * @param list
     *            Zu negierende Menge.
     * @param total
     *            Gesamtmenge.
     * @return Komplementärmenge.
     */
    static List<Integer> negateList(List<Integer> list, List<Integer> total) {
        List<Integer> negatedList = new ArrayList<>();

        for (Integer index : total) {
            if (!list.contains(index)) {
                negatedList.add(index);
            }
        }

        return negatedList;
    }

    /**
     * Diese Methode bildet die Schnittmenge zweier Listen mit Indices.
     *
     * @param list1
     *            Menge 1.
     * @param list2
     *            Menge 2.
     * @return Schnittmenge.
     */
    static List<Integer> intersectLists(List<Integer> list1, List<Integer> list2) {
        List<Integer> intersection = new ArrayList<>();

        for (Integer index : list1) {
            if (list2.contains(index)) {
                intersection.add(index);
            }
        }

        return intersection;
    }

    /**
     * Diese Methode bildet die Vereinigung zweier Listen mit Indices.
     *
     * @param list1
     *            Menge 1.
     * @param list2
     *            Menge 2.
     * @return Vereinigung.
     */
    static List<Integer> uniteLists(List<Integer> list1, List<Integer> list2) {
        List<Integer> union = new ArrayList<>();
        union.addAll(list1);

        for (Integer index : list2) {
            if (!list1.contains(index) )
                union.add(index);
        }
        Collections.sort(union);

        return union;
    }

    /**
     * Erzeugt mit Hilfe einer Methode und der Menge der Datensätze eine Liste, die dem Kriterium
     * dieser Liste genügt.
     *
     * @param method
     *            Filtermethode
     * @param list
     *            Liste mit den Datensätzen
     * @return Liste mit Indices (Start bei 1)
     * @throws FilterException
     *             Bei Fehlern wird diese Ausnahme geworfen.
     */
    private List<Integer> createListByMethod(Method<Data> method, List<Data> datasets) {
        return method.buildFilter(datasets);
    }

}
