package de.duehl.basics.io.textfile.dictionary;

/*
 * Copyright 2025 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.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import de.duehl.basics.collections.CollectionsHelper;

/**
 * Diese Klasse stellt ein Wörterbuch dar.
 *
 * Ein Wörterbuch besteht aus einer Liste von Wörterbucheinträgen, welche jeweils einen
 * Hauptwort (z.B. eine deutsche Vokabel) und eine Reihe von alternativen Worten (z.B.
 * englische Übersetzungen) enthalten. Die Alternativen können auch leer sein, der Hauptwort
 * muss vorhanden und nicht leer sein.
 *
 * @version 1.01     2025-07-01
 * @author Christian Dühl
 */

public class Dictionary implements Iterable<DictionaryEntry> {

    /** Die Liste der Wörterbucheinträge. */
    private final List<DictionaryEntry> dictionaryEntries;

    /** Konstruktor. */
    public Dictionary() {
        dictionaryEntries = new ArrayList<>();
    }

    /** Fügt einen Wörterbucheintrag hinzu. */
    public void addDictionaryEntry(DictionaryEntry dictionaryEntry) {
        dictionaryEntries.add(dictionaryEntry);
    }

    @Override
    public Iterator<DictionaryEntry> iterator() {
        return dictionaryEntries.iterator();
    }

    /** Getter für die Liste der Wörterbucheinträge. */
    public List<DictionaryEntry> getDictionaryEntries() {
        return dictionaryEntries;
    }

    /**
     * Gibt eine Beschreibung zur Anzeige in Fehlermeldungen oder Debug-Ausgaben zurück.
     *
     * @param title
     *            Eine Überschrift, die über der Ausgabe angezeigt wird.
     * @return Beschreibung des Wörterbuchs.
     */
    public String createDescription(String title) {
        StringBuilder builder = new StringBuilder();
        builder.append(title).append(":\n");
        builder.append("\n");
        builder.append(createDescription(4));
        return builder.toString();
    }

    /**
     * Gibt eine Beschreibung zum Speichern von Wörterbüchern oder dergleichen zurück.
     *
     * @return Beschreibung des Wörterbuchs.
     */
    public String createDescription() {
        return createDescription(0);
    }

    private String createDescription(int indentation) {
        StringBuilder builder = new StringBuilder();
        boolean first = true;
        for (DictionaryEntry entry : dictionaryEntries) {
            if (first) {
                first = false;
            }
            else {
                builder.append("\n");
            }
            builder.append(entry.createDescription(indentation));
        }
        return builder.toString();
    }

    /**
     * Gibt an, ob das Wörterbuch einen Eintrag mit dem übergebenen Hauptwort besitzt.
     *
     * Anmerkung: Es wird nicht verhindert, dass es mehrere Einträge mit den gleichen Hauptworten
     * geben kann.
     *
     * @param mainWord
     *            Das Hauptwort.
     * @return Wahrheitswert: true genau dann, wenn es einen Eintrag mit dem gesuchten Hauptwort
     *         gibt.
     */
    public boolean containsEntryWithMainWord(String mainWord) {
        for (DictionaryEntry entry : dictionaryEntries) {
            if (mainWord.equals(entry.getMainWord())) {
                return true;
            }
        }

        return false;
    }

    /**
     * Gibt an, ob das Wörterbuch einen Eintrag mit dem übergebenen Begriff als Hauptwort oder
     * Alternative besitzt.
     *
     * @param mainWordOrAlternative
     *            Das Hauptwort oder eine Alternative.
     * @return Wahrheitswert: true genau dann, wenn es einen Eintrag mit dem gesuchten Hauptwort
     *         oder einer Alternative gibt.
     */
    public boolean containsEntryWithMainWordOrAlternative(String mainWordOrAlternative) {
        for (DictionaryEntry entry : dictionaryEntries) {
            if (entry.contains(mainWordOrAlternative)) {
                return true;
            }
        }

        return false;
    }

    /** Prüft ob der übergebene Text ein Hauptwort oder eine der Alternativen enthält. */
    public boolean textContainsMainWordOrAlternative(String text) {
        for (DictionaryEntry entry : dictionaryEntries) {
            if (entry.textContainsMainWordOrAlternative(text)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Gibt den ersten Eintrag mit dem übergebenen Hauptwort zurück.
     *
     * Anmerkung: Es wird nicht verhindert, dass es mehrere Einträge mit den gleichen Hauptworten
     * geben kann. Hier wird der erste Eintrag zurückgegeben.
     *
     * Man sollte erst mit containsEntryWithMainWord(String) prüfen, ob es solch einen Eintrag
     * gibt. Wird nämlich keiner gefunden, wird eine Ausnahme geworfen.
     *
     * @param mainWord
     *            Das Hauptwort.
     * @return Der erste gefundene Eintrag mit dem Hauptwort.
     */
    public DictionaryEntry getEntryWithMainWord(String mainWord) {
        for (DictionaryEntry entry : dictionaryEntries) {
            if (mainWord.equals(entry.getMainWord())) {
                return entry;
            }
        }

        throw new RuntimeException(
                "Es wurde kein Wörterbucheintrag mit dem Hauptwort '" + mainWord + "' gefunden.");
    }

    /** Gibt die Anzahl der Wörterbucheinträge zurück. */
    public int size() {
        return dictionaryEntries.size();
    }

    /** Gibt den Wörterbucheintrag mit dem übergebenen Index zurück. */
    public DictionaryEntry get(int index) {
        return dictionaryEntries.get(index);
    }

    /**
     * Gibt das Hauptwort zurück, falls die Alternative bekannt ist, anderenfalls wird die
     * übergebene Alternative unverändert zurückgegeben.
     *
     * @param alternative
     *            Die Alternative, zu der das Hauptwort zurückgegeben werden soll.
     */
    public String getMainWordOfAlternative(String alternative) {
        for (DictionaryEntry entry : dictionaryEntries) {
            if (entry.contains(alternative)) {
                return entry.getMainWord();
            }
        }

        return alternative;
    }

    /**
     * Gibt das Hauptwort zurück, falls die Alternative bekannt ist, anderenfalls wird der leere
     * String zurückgegeben.
     *
     * @param alternative
     *            Die Alternative, zu der das Hauptwort zurückgegeben werden soll.
     */
    public String getMainWordOfAlternativeToEmptyIfNotFound(String alternative) {
        for (DictionaryEntry entry : dictionaryEntries) {
            if (entry.contains(alternative)) {
                return entry.getMainWord();
            }
        }

        return "";
    }

    /**
     * Gibt das Hauptwort zurück, falls die Alternative bekannt ist, anderenfalls wird eine
     * Ausnahme geworfen.
     *
     * @param alternative
     *            Die Alternative, zu der das Hauptwort zurückgegeben werden soll.
     * @param errorDescription
     *            Die Beschreibung des Fehlers für die Ausnahme, die geworfen wird, wenn die
     *            Alternative nicht bekannt ist.
     */
    public String getMainWordOfAlternativeWithError(String alternative, String errorDescription) {
        for (DictionaryEntry entry : dictionaryEntries) {
            if (entry.contains(alternative)) {
                return entry.getMainWord();
            }
        }

        throw new RuntimeException(errorDescription);
    }

    /** Sortiert die Alternativen alphabetisch. */
    public void sortAlternatives() {
        for (DictionaryEntry entry : dictionaryEntries) {
            entry.sortAlternatives();
        }
    }

    /**
     * Sortiert die Alternativen absteigend nach der Länge. Bei gleicher Länge wird alphabetisch
     * sortiert.
     */
    public void sortAllAlternativesByLengthDescanding() {
        for (DictionaryEntry entry : dictionaryEntries) {
            entry.sortAllAlternativesByLengthDescanding();
        }
    }

    /**
     * Gibt das Wörterbuch als Liste von Listen zurück, wie es ursprünglich immer verwendet wurde.
     */
    public List<List<String>> toListOfLists() { // nur für alten Kram den ich nicht komplett
                                                // anpassen mag
        List<List<String>> listOfLists = new ArrayList<>();

        for (DictionaryEntry entry : dictionaryEntries) {
            listOfLists.add(entry.getAsFlatList());
        }

        return listOfLists;
    }

    /** Erzeugt eine flache Liste mit allen Hauptworten und Alternativen. */
    public List<String> toFlatList() {
        List<String> list = new ArrayList<>();

        for (DictionaryEntry entry : dictionaryEntries) {
            list.add(entry.getMainWord());
            list.addAll(entry.getAlternatives());
        }

        return list;
    }

    /**
     * Erzeugt eine flache Liste mit allen Hauptworten und Alternativen, welche disjunkt gemacht
     * und absteigend nach der Länge sortiert wurde.
     */
    public List<String> toDisjunctFlatListSortedByLengthDescanding() {
        List<String> list = toFlatList();
        CollectionsHelper.makeListDisjunct(list);
        CollectionsHelper.sortStringListByLengthDescanding(list);
        return list;
    }

    /**
     * Erzeugt eine HashMap, bei der die Hauptworte auf Listen bestehend aus Hauptwort und
     * Alternativen bestehen.
     */
    public Map<String, List<String>> createMapFromDictionary() {
        boolean multipleMainWordsAreError = false;
        return createMapFromDictionaryInternal(multipleMainWordsAreError);
    }

    /**
     * Erzeugt eine HashMap, bei der die Hauptworte auf Listen bestehend aus Hauptwort und
     * Alternativen bestehen.
     *
     * Bei Wörterbucheinträgen mit gleichen Hauptworten wird eine Ausnahme geworfen.
     */
    public Map<String, List<String>> createMapFromDictionaryMultipleMainWordsAreError() {
        boolean multipleMainWordsAreError = true;
        return createMapFromDictionaryInternal(multipleMainWordsAreError);
    }

    private Map<String, List<String>> createMapFromDictionaryInternal(
            boolean multipleMainWordsAreError) {
        Map<String, List<String>> map = new HashMap<>();

        for (DictionaryEntry entry : dictionaryEntries) {
            String key = entry.getMainWord();
            if (map.containsKey(key) && multipleMainWordsAreError) {
                throw new RuntimeException("Doppelter Schlüssel '" + key + "' in Wörterbuch");
            }
            map.put(key, entry.getAsFlatList());
        }

        return map;
    }

    /** Gibt eine Liste mit den Hauptworten zurück. */
    public List<String> getMainWords() {
        List<String> mainWords = new ArrayList<>();

        for (DictionaryEntry entry : dictionaryEntries) {
            String mainWord = entry.getMainWord();
            mainWords.add(mainWord);
        }

        return mainWords;
    }

    /**
     * Mit dieser Methode wird überprüft, dass alle Hauptworte und Alternativen als ganzes über
     * alle einzelne Wörterbucheinträge nur genau einmal vorkommen.
     */
    public void checkAllMaiwordsAndAlternativesAreUnique() {
        List<String> seenWords = new ArrayList<>();
        List<String> multipleWords = new ArrayList<>();

        for (DictionaryEntry entry : dictionaryEntries) {
            String mainWord = entry.getMainWord();
            if (seenWords.contains(mainWord)) {
                multipleWords.add(mainWord);
            }
            seenWords.add(mainWord);
            for (String alternative : entry.getAlternatives()) {
                if (seenWords.contains(alternative)) {
                    multipleWords.add(alternative);
                }
                seenWords.add(alternative);
            }
        }

        if (!multipleWords.isEmpty()) {
            throw new RuntimeException("Die folgenden Einträge kommen mehr als einmal im "
                    + "Dictionary vor:\n"
                    + CollectionsHelper.listListNice(multipleWords));
        }
    }

    @Override
    public int hashCode() {
        return Objects.hash(dictionaryEntries);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Dictionary other = (Dictionary) obj;
        return Objects.equals(dictionaryEntries, other.dictionaryEntries);
    }

    @Override
    public String toString() {
        return "Dictionary [dictionaryEntries=" + dictionaryEntries + "]";
    }

}
