package de.duehl.basics.collections;

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

import de.duehl.basics.text.NumberString;

/**
 * Diese Klasse vergleicht zwei Listen von Strings derart, dass eine aussagekräftige Meldung über
 * den Unterschied zurückgegeben wird.
 *
 * Geprüft wird:
 *     - Größe der Liste
 *     - Elemente der einen Liste nicht in der anderen Liste enthalten.
 *       Hier wird in beide Richtungen geprüft, damit die Fehlermeldung vollständig ist.
 *     - Reihenfolge anders
 *
 * Braucht man diese nicht, reicht auch einfach list1.equals(list2).
 *
 * @version 1.01     2024-10-14
 * @author Christian Dühl
 */

public class StringListComparer {

    /** Die erste zu vergleichende Liste. */
    private final List<String> list1;

    /** Die zweite zu vergleichende Liste. */
    private final List<String> list2;

    /**
     * Die Beschreibung der Elemente der ersten Liste für den Unterschied in der Art "der ersten
     * Liste".
     */
    private String description1;

    /**
     * Die Beschreibung der Elemente der zweiten Liste für den Unterschied in der Art "der zweiten
     * Liste".
     */
    private String description2;

    /** Gibt an, ob die beiden Listen gleich sind. */
    private boolean equal;

    /**
     * Enthält den Grund dafür, warum die Listen nicht gleich sind.
     *
     * Sind sie gleich, ist der Wert der leere String.
     */
    private String notEqualReason;

    /**
     * Konstruktor.
     *
     * @param list1
     *            Die erste zu vergleichende Liste.
     * @param list2
     *            Die zweite zu vergleichende Liste.
     */
    public StringListComparer(List<String> list1, List<String> list2) {
        this.list1 = list1;
        this.list2 = list2;

        description1 = "der ersten Liste";
        description2 = "der zweiten Liste";
    }

    /**
     * Setter für die Beschreibung der Elemente der ersten Liste für den Unterschied in der Art
     * "der ersten Liste".
     */
    public void setDescription1(String description1) {
        this.description1 = description1;
    }

    /**
     * Setter für die Beschreibung der Elemente der zweiten Liste für den Unterschied in der Art
     * "der zweiten Liste".
     */
    public void setDescription2(String description2) {
        this.description2 = description2;
    }

    /** Führt den Vergleich durch. */
    public void compare() {
        init();
        if (equal) checkDifferentSizes();
        if (equal) checkNotContainedInOtherList();
        if (equal) checkDifferentSortOrder();
    }

    private void init() {
        equal = true;
        notEqualReason = "";
    }

    /** Prüft die Größe der beiden Listen auf Gleichheit. */
    private void checkDifferentSizes() {
        int size1 = list1.size();
        int size2 = list2.size();

        if (size1 != size2) {
            equal = false;
            notEqualReason = "Die beiden Listen haben unterschiedlich viele Elemente. In "
                    + description1 + " " + istSind(size1) + " " + NumberString.taupu(size1) + " "
                    + xElementeText(size1) + " und in " + description2 + " " + istSind(size2) + " "
                    + NumberString.taupu(size2) + " " + xElementeText(size2) + ".";
        }
    }

    /**
     * Prüft für beide Listen, ob alle ihre Elemente in der anderen Liste enthalten sind, um eine
     * vollständige Beschreibung des Unterschieds zu erhalten.
     */
    private void checkNotContainedInOtherList() {
        List<String> elementsNotContainedInList2 = new ArrayList<>();
        for (String element1 : list1) {
            if (!list2.contains(element1)) {
                elementsNotContainedInList2.add(element1);
            }
        }

        List<String> elementsNotContainedInList1 = new ArrayList<>();
        for (String element2 : list2) {
            if (!list1.contains(element2)) {
                elementsNotContainedInList1.add(element2);
            }
        }

        /*
         * Nur einseitig nicht in der anderen Liste enthalten kann vorkommen, wenn die Inhalte
         * der Listen nicht disjunkt sind. Da kann die Anzahl gleich sein, aber trotzdem etwas
         * fehlen:
         */
        if (elementsNotContainedInList2.isEmpty() && elementsNotContainedInList1.isEmpty()) {
            // gut, wir prüfen weiter.
        }
        else if (elementsNotContainedInList2.isEmpty()) {
            equal = false;
            int missingSize = elementsNotContainedInList1.size();
            notEqualReason = "Es " + istSind(missingSize) + " " + NumberString.taupu(missingSize)
                    + " " + xElementeText(missingSize) + " in " + description1 + " nicht in "
                    + description2 + " enthalten:\n"
                    + CollectionsHelper.listListNice(elementsNotContainedInList1);
        }
        else if (elementsNotContainedInList1.isEmpty()) {
            equal = false;
            int missingSize = elementsNotContainedInList2.size();
            notEqualReason = "Es " + istSind(missingSize) + " " + NumberString.taupu(missingSize)
                    + " " + xElementeText(missingSize) + " in " + description2 + " nicht in "
                    + description1 + " enthalten:\n"
                    + CollectionsHelper.listListNice(elementsNotContainedInList2);
        }
        else {
            equal = false;
            int missingSize1 = elementsNotContainedInList1.size();
            int missingSize2 = elementsNotContainedInList2.size();
            notEqualReason = "Es " + istSind(missingSize1) + " " + NumberString.taupu(missingSize1)
                    + " " + xElementeText(missingSize1) + " in "
                    + description1 + " nicht in " + description2 + " enthalten:\n"
                    + CollectionsHelper.listListNice(elementsNotContainedInList1)
                    + "\n"
                    + "Außerdem " + istSind(missingSize2) + " " + NumberString.taupu(missingSize2)
                    + " " + xElementeText(missingSize2) + " in "
                    + description2 + " nicht in " + description1 + " enthalten:\n"
                    + CollectionsHelper.listListNice(elementsNotContainedInList2);
        }
    }

    /**
     * Prüft die Sortierung der beiden Listen auf gleichheit. Hier wissen wir, dass beide Listen
     * gleich groß sind und dass alle Elemente der einen Liste in der anderen enthalten sind. DAher
     * reicht es einfach an jedem Index zu schauen, ob die Inhalte gleich sind.
     */
    private void checkDifferentSortOrder() {
        List<Integer> indicesWithDifferences = new ArrayList<>();
        for (int index = 0; index < list1.size(); ++index) {
            String element1 = list1.get(index);
            String element2 = list2.get(index);
            if (!element1.equals(element2)) {
                indicesWithDifferences.add(index);
            }
        }

        if (!indicesWithDifferences.isEmpty()) {
            equal = false;
            StringBuilder builder = new StringBuilder();
            builder.append("An den folgenden Positionen (1-basiert) weichen die beiden Listen "
                    + "von einander ab:\n");
            for (int index : indicesWithDifferences) {
                builder.append("    " + (index + 1) + ": '" + list1.get(index) + "' <-> '"
                        + list2.get(index) + "'\n");
            }

            notEqualReason = builder.toString();
        }
    }

    private String xElementeText(int size) {
        return NumberString.germanPlural(size, "Elemente", "Element");
    }

    private String istSind(int size) {
        return NumberString.germanPlural(size, "sind", "ist");
    }

    /** Gibt an, ob die beiden Listen gleich sind. */
    public boolean isEqual() {
        return equal;
    }

    /**
     * Getter für den Grund dafür, warum die Listen nicht gleich sind.
     *
     * Sind sie gleich, ist der Wert der leere String.
     */
    public String getNotEqualReason() {
        return notEqualReason;
    }

}
