package de.duehl.vocabulary.japanese.logic.ownlists;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

import de.duehl.basics.collections.CollectionsHelper;
import de.duehl.basics.io.Charset;
import de.duehl.basics.io.FileHelper;
import de.duehl.basics.text.NumberString;
import de.duehl.basics.text.Text;
import de.duehl.swing.ui.GuiTools;
import de.duehl.vocabulary.japanese.common.persistence.Options;
import de.duehl.vocabulary.japanese.common.persistence.data.OwnListPersistanceData;
import de.duehl.vocabulary.japanese.data.OwnList;
import de.duehl.vocabulary.japanese.data.Vocable;
import de.duehl.vocabulary.japanese.data.Vocabulary;
import de.duehl.vocabulary.japanese.io.OwnListReader;
import de.duehl.vocabulary.japanese.io.OwnListWriter;
import de.duehl.vocabulary.japanese.logic.internal.InternalDataRequester;
import de.duehl.vocabulary.japanese.ui.VocabularyTrainerGui;
import de.duehl.vocabulary.japanese.ui.data.OwnListInGuiRefresher;

import static de.duehl.vocabulary.japanese.ui.dialog.ComplexVocableSearchDialog.COMPLEX_SEARCH_NAME_START;

/**
 * Diese Klasse stellt die Logik zu den eigenen Listen von Vokabeln dar.
 *
 * Die Listen basieren auf dem keys der Vokabeln.  Siehe VocabularyTools.createVocableKey(vocable),
 * die Grundlage ist die Zeichenkette in Kana (Hiragana oder Katakana) und die erste Übersetzung.
 *
 * Die Vokabellisten werden im Benutzerverzeichnis in ~/own_vocable_lists gespeichert. Dazu gibt
 * es eine Datei mit der Auflistung aller Vokabellisten in der richtigen Reihenfolge, sowie je
 * eine Datei pro Liste, die eine Liste der keys enthält, pro Zeile einen.
 *
 * @version 1.01     2025-11-26
 * @author Christian Dühl
 */

public class OwnLists {

    /**
     * Der Name der Datei mit den Namen der abgespeicherten Listen (ist für die Reihenfolge
     * wichtig).
     */
    private static final String LIST_NAMES_FILENAME =
            FileHelper.concatPathes(OwnList.OWN_LIST_DIRECTORY, "vokabellisten.nam");


    /** Die Liste mit den bekannten Vokabularien. */
    private final List<Vocabulary> vocabularies;

    /** Das Verzeichnis der Vokabeln nach ihren Schlüsseln. */
    private final Map<String, Vocable> keyToVocable;

    /** Das Objekt das zu einer Vokabel die internen, benutzerabhängigen Daten abrufen kann. */
    private final InternalDataRequester requester;

    /** Die Programmoptionen. */
    private final Options options;

    /** Die grafische Oberfläche. */
    private final VocabularyTrainerGui gui;

    /** Die Liste mit den eigenen Vokabellisten (diese werden hier verwaltet). */
    private final List<OwnList> ownLists;

    /** Die Liste der unikaten Suchbegriffe aller Vokabeln. */
    private List<String> searchWords;

    /** Die Liste der unikaten Wortarten aller Vokabeln. */
    private List<String> partsOfSpeach;

    /**
     * Das Objekt, das die eigenen Listen in der grafischen Oberfläche des Vokabel-Trainers auf die
     * eine oder andere Weise aktualisieren kann.
     */
    private OwnListInGuiRefresher ownListInGuiRefresher;

    /**
     * Konstruktor.
     *
     * @param vocabularies
     *            Die Liste mit den bekannten Vokabularien.
     * @param keyToVocable
     *            Das Verzeichnis der Vokabeln nach ihren Schlüsseln.
     * @param requester
     *            Das Objekt das zu einer Vokabel die internen, benutzerabhängigen Daten abrufen
     *            kann.
     * @param options
     *            Die Programmoptionen.
     * @param gui
     *            Die grafische Oberfläche.
     */
    public OwnLists(List<Vocabulary> vocabularies, Map<String, Vocable> keyToVocable,
            InternalDataRequester requester, Options options, VocabularyTrainerGui gui) {
        this.vocabularies = vocabularies;
        this.keyToVocable = keyToVocable;
        this.requester = requester;
        this.options = options;
        this.gui = gui;

        OwnList.createOwnListDirectoryIfNotExisting();

        ownLists = new ArrayList<>();
        readStoredOwnLists();

        determineSearchWords();
        determinePartsOfSpeach();
    }

    /**
     * Liest die im benutzerspezifische Verzeichnis gespeicherten Listen beim Start des Programms
     * ein.
     */
    private void readStoredOwnLists() {
        ownLists.clear();

        if (FileHelper.isFile(LIST_NAMES_FILENAME)) {
            List<String> names = readOwnListNames();
            ownLists.addAll(readStoredOwnLists(names));
        }
    }

    /** Liest die Datei mit den Namen der eigenen Listen ein. */
    public static List<String> readOwnListNames() {
        return FileHelper.readFileToList(LIST_NAMES_FILENAME, Charset.UTF_8);
    }

    private List<OwnList> readStoredOwnLists(List<String> names) {
        List<OwnList> ownLists = new ArrayList<>();

        for (String name : names) {
            OwnList ownList = readOwnList(name);
            ownLists.add(ownList);
        }

        return ownLists;
    }

    private OwnList readOwnList(String name) {
        OwnListReader reader = new OwnListReader(name, keyToVocable);
        reader.read();
        return reader.getOwnList();
    }

    private void determineSearchWords() {
        searchWords = new ArrayList<>();

        for (Vocabulary vocabulary : vocabularies) {
            for (Vocable vocable : vocabulary.getVocables()) {
                for (String searchWord : vocable.getSearchWords()) {
                    if (!searchWord.isBlank() && !searchWords.contains(searchWord)) {
                        searchWords.add(searchWord);
                    }
                }
            }
        }

        CollectionsHelper.sortBetterAlphabethical(searchWords);

    }

    private void determinePartsOfSpeach() {
        partsOfSpeach = new ArrayList<>();

        for (Vocabulary vocabulary : vocabularies) {
            for (Vocable vocable : vocabulary.getVocables()) {
                for (String partOfSpeach : vocable.getPartsOfSpeech()) {
                    if (!partOfSpeach.isBlank() && !partsOfSpeach.contains(partOfSpeach)) {
                        partsOfSpeach.add(partOfSpeach);
                    }
                }
            }
        }

        CollectionsHelper.sortBetterAlphabethical(partsOfSpeach);

    }

    /**
     * Setter für das Objekt, das die eigenen Listen in der grafischen Oberfläche des
     * Vokabel-Trainers auf die eine oder andere Weise aktualisieren kann.
     */
    public void setOwnListInGuiRefresher(OwnListInGuiRefresher ownListInGuiRefresher) {
        this.ownListInGuiRefresher = ownListInGuiRefresher;
    }

    /** Speichert die Listen im im benutzerspezifische Verzeichnis ab. */
    public synchronized void storeOwnLists() {
        new Thread(() -> storeOwnListsInOwnThread()).start();
    }

    private synchronized void storeOwnListsInOwnThread() {
        storeNames();
        storeLists();
    }

    private void storeNames() {
        List<String> names = new ArrayList<>();

        for (OwnList ownList : ownLists) {
            String name = ownList.getName();
            names.add(name);
        }

        FileHelper.writeLinesToFile(names, LIST_NAMES_FILENAME, Charset.UTF_8);
    }

    private void storeLists() {
        for (OwnList ownList : ownLists) {
            storeList(ownList);
        }
    }

    private void storeList(OwnList ownList) {
        OwnListWriter writer = new OwnListWriter(ownList, requester);
        writer.write();
    }

    /** Getter für die Liste mit den eigenen Vokabellisten. */
    public List<OwnList> getOwnLists() {
        return ownLists;
    }

    /** Gibt an, ob ein Name als neuer Name einer Vokabelliste brauchbar ist. */
    public boolean nameCheckOk(String name) {
        String namePart = Text.createJavaVariableName(name);
        return !name.isBlank()
                && !namePart.isBlank()
                && !isNameKnown(name);
    }

    /** Prüft ob es bereits eine Liste mit diesem Namen gibt. */
    private boolean isNameKnown(String name) {
        for (OwnList ownList : ownLists) {
            String ownListName = ownList.getName();
            if (name.equals(ownListName)) {
                return true;
            }
        }

        return false;
    }

    /** Erzeugt eine neue, leere Liste mit dem angegebenen Namen. */
    public OwnList createNewList(String name) {
        OwnList ownList = new OwnList(name, OwnListReader.DEFAULT_OWN_LIST_CATEGORY,
                OwnListReader.DEFAULT_OWN_LIST_SUB_CATEGORY, new ArrayList<>());
        ownLists.add(ownList);
        storeList(ownList);
        return ownList;
    }

    /** Löscht die Vokabelliste aus der Liste und von der Festplatte. */
    public void deleteOwnList(OwnList ownList) {
        String numberOfVocables = NumberString.taupu(ownList.getVocables().size());
        String message = "Wollen sie wirklich die Liste '" + ownList.getName() + "' mit "
                + numberOfVocables + " Vokabeln entfernen?\n\n"
                + "Sie wird auch von der Festplatte gelöscht!\n\n"
                + "Die Vokabeln verbleiben in den Vokabularien, aber die Zusammenstellung in\n"
                + "Form dieser Vokabelliste wird beim Bestätigen des Dialogs entfernt.";
        String title = "Soll die Vokabelliste wirklich gelöscht werden?";
        boolean delete = GuiTools.askUser(title, message);
        if (delete) {
            reallyDeleteOwnList(ownList);
        }
    }

    private void reallyDeleteOwnList(OwnList ownList) {
        int index = ownLists.indexOf(ownList);
        ownLists.remove(index);

        String filename = ownList.determineFilename();
        FileHelper.deleteFileIfExistent(filename);
    }

    /** Importiert eine Vokabelliste. */
    public void importOwnList() {
        String importFilename = GuiTools.openFile(
                options.getLastUsedOwnListImportExportDirectory(),
                OwnList.createFileFilter());
        if (!importFilename.isEmpty()) {
            if (FileHelper.isFile(importFilename)) {
                importExistingFile(importFilename);
            }
            else {
                GuiTools.informUser("Datei nicht gefunden",
                        "Die Datei '" + importFilename + "' wurde nicht gefunden.\n"
                                + "Der Import wurde abgebrochen.");
            }
        }
    }

    private void importExistingFile(String importFilename) {
        String name = GuiTools.askUserToEnterAStringValue("Bitte geben die den Namen ein",
                "Bitte geben sie den Namen für die zu importierende Liste ein",
                OwnList.createJavaFormOfNameFromFilename(importFilename));
        if (isNameUsed(name)) {
            GuiTools.informUser("Der Name gehört zu einer Liste", "Der Name '" + name
                    + "' gehört zu einer bereits vorhandenen Liste.\n"
                    + "Der Import wurde abgebrochen.");
        }
        else {
            storeLastUsedImportExportDirectoryIntoOptions(importFilename);
            reallyImportFile(importFilename, name);
        }
    }

    private boolean isNameUsed(String name) {
        OwnList ownList = findByName(name);
        return !ownList.equals(NO_OWN_LIST_FOUND);
    }

    public static final OwnList NO_OWN_LIST_FOUND = new OwnList(
            "###NOT-FOUND-NAME###", "###NOT-FOUND-KATEGORY###", "###NOT-FOUND-SUBKATEGORY###",
            new ArrayList<>());

    /**
     * Gibt die zum Namen gehörige Liste zurück. Wurde keine mit diesem NAmen gefunden, wird
     * NO_OWN_LIST_FOUND zurückgegeben.
     *
     * Der Name ist eindeutig, denn bei der Erstellung von eigenen Listen werden gleiche Namen
     * verhindert!
     */
    public OwnList findByName(String name) {
        for (OwnList ownList : ownLists) {
            String nameFromOwnList = ownList.getName();
            if (name.equals(nameFromOwnList)) {
                return ownList;
            }
        }

        return NO_OWN_LIST_FOUND;
    }

    private void reallyImportFile(String importFilename, String name) {
        String filename = OwnList.determineFilename(name);
        FileHelper.copyFile(importFilename, filename);

        OwnList ownList = readOwnList(name);
        ownLists.add(ownList);

        storeOwnLists(); // Datei mit den Namen speichern
    }

    /**
     * Entfernt alle zu einer Gruppe gehörigen Eigenen Listen.
     *
     * @param groupStart
     *            Der Beginn der Gruppennamen, z.B. "Marugoto A1".
     */
    public void deleteOwnListsOfGroup(String groupStart) {
        if (groupStart.isEmpty()) {
            throw new RuntimeException("Leere Gruppenanfänge sind nicht zulässig, sonst würden "
                    + "alle eigenen Listen entfernt!");
        }

        List<OwnList> ownListsToDelete = new ArrayList<>();
        for (OwnList ownList:  ownLists) {
            String name = ownList.getName();
            if (name.startsWith(groupStart)) {
                ownListsToDelete.add(ownList);
            }
        }

        for (OwnList ownList: ownListsToDelete) {
            reallyDeleteOwnList(ownList);
        }
    }

    /**
     * Importiert eine Liste aus einer Gruppe von eigenen Listen, welche von der Webseite
     * heruntergeladen und ausgepackt wurde.
     *
     * @param importFilename
     *            Der Dateiname mit Pfad, unter dem die Liste im ausgepackten Verzeichnis des
     *            ZIP-Archivs liegt.
     * @param name
     *            Der Name der Liste.
     */
    public void importOwnListFromGroup(String importFilename, String name) {
        deleteOwnListByName(name);
        reallyImportFile(importFilename, name);
    }

    /**
     * Entfernt die eigene Liste mit dem übergebenen Namen, falls sie bekannt ist, sowohl aus der
     * Verwaltung als auch auf der lokalen Festplatte im Benutzerverzeichnis.
     */
    private void deleteOwnListByName(String name) {
        OwnList ownList = findByName(name);
        if (!ownList.equals(NO_OWN_LIST_FOUND)) {
            ownLists.remove(ownList);
            String filename = OwnList.determineFilename(name);
            FileHelper.deleteFile(filename);
        }
    }

    /** Exportiert die Vokabelliste. */
    public void exportList(OwnList ownList) {
        String exportFilename = GuiTools.saveFileAsWithTitle(
                "Wohin soll die Vokabelliste '" + ownList.getName() + "' exportiert werden?",
                null,
                options.getLastUsedOwnListImportExportDirectory(),
                OwnList.createFileFilter(),
                FileHelper.getBareName(ownList.determineFilename()));
        if (!exportFilename.isBlank()) {
            boolean export = false;
            if (FileHelper.exists(exportFilename)) {
                export = GuiTools.askUser("Die Datei existiert bereits.", "Die Datei '"
                        + exportFilename + "' existiert bereits, soll sie überschriben werden?");
            }
            else {
                export = true;
            }
            if (export) {
                reallyExportList(ownList, exportFilename);
                gui.setMessageLater("Exportiert: " + FileHelper.getBareName(exportFilename));
            }
        }
    }

    private void reallyExportList(OwnList ownList, String exportFilename) {
        String filename = ownList.determineFilename();
        reallyExportList(filename, exportFilename);
    }

    private void reallyExportList(String filename, String exportFilename) {
        FileHelper.copyFile(filename, exportFilename);

        storeLastUsedImportExportDirectoryIntoOptions(exportFilename);
    }

    private void storeLastUsedImportExportDirectoryIntoOptions(String imOrExportFilename) {
        String dirname = FileHelper.getDirName(imOrExportFilename);
        options.setLastUsedOwnListImportExportDirectory(dirname);
    }

    /** Alle eigenen Vokabellisten exportieren. */
    public void exportAllOwnLists() {
        String exportDirectory = GuiTools.saveDirectory(
                options.getLastUsedOwnListImportExportDirectory(),
                "In welches Verzeichnis sollen die Vokabellisten exportiert werden?",
                gui.getWindowAsComponent());
        if (!exportDirectory.isBlank()) {
            List<String> filesToExport = new ArrayList<>();
            filesToExport.add(LIST_NAMES_FILENAME);
            for (OwnList ownList : ownLists) {
                filesToExport.add(ownList.determineFilename());
            }

            List<String> exportedFiles = new ArrayList<>();
            for (String filename : filesToExport) {
                boolean exported = exportInDirectory(filename, exportDirectory);
                if (exported) {
                    exportedFiles.add(filename);
                }
            }

            gui.setMessageLater(exportedFiles.size() + " Dateien exportiert.");
        }
    }

    private boolean exportInDirectory(String filename, String exportDirectory) {
        String bareFilename = FileHelper.getBareName(filename);
        String exportFilename = FileHelper.concatPathes(exportDirectory, bareFilename);

        boolean export = false;
        if (FileHelper.exists(exportFilename)) {
            export = GuiTools.askUser("Die Datei existiert bereits.", "Die Datei '"
                    + exportFilename + "' existiert bereits, soll sie überschriben werden?");
        }
        else {
            export = true;
        }

        if (export) {
            reallyExportList(filename, exportFilename);
        }

        return export;
    }

    /** Beim Umbenennen sollte man auch die zugehörige Datei umbenennen, das passiert hier. */
    public void renameListFile(String oldName, String newName) {
        String oldFilename = OwnList.determineFilename(oldName);
        String newFilename = OwnList.determineFilename(newName);

        /*
         * Falls man direkt schon beim Anlegen einer Liste diese umbenennt, fehlt die Datei noch.
         * Daher wird hier geprüft, ob sie vorhanden ist:
         */
        if (FileHelper.exists(oldFilename)) {
            FileHelper.moveFile(oldFilename, newFilename);
        }
    }

    /**
     * Speichert die abgefragten Vokabeln als Liste.
     *
     * @param vocables
     *            Die Liste der abzuspeichernden Vokabeln.
     * @param nameSuggestion
     *            Ein Vorschlag für den Namen der Liste.
     */
    public void saveAsList(List<Vocable> vocables, String nameSuggestion) {
        String category;
        if (nameSuggestion.startsWith(COMPLEX_SEARCH_NAME_START)) {
            category = Text.firstCharToLowerCase(COMPLEX_SEARCH_NAME_START);
        }
        else {
            category = "abgefragte Listen";
        }
        OwnList ownList = new OwnList(nameSuggestion, category,
                OwnListReader.DEFAULT_OWN_LIST_SUB_CATEGORY, vocables);
        ownLists.add(ownList);
        storeOwnLists(); // Datei mit dem Namen speichern
    }

    /** Speichert die Optionen. */
    public void saveOptions() {
        gui.saveOptions();
    }

    /** Getter für die Liste der unikaten Suchbegriffe aller Vokabeln. */
    public List<String> getSearchWords() {
        return searchWords;
    }

    /** Getter für die Liste der unikaten Wortarten aller Vokabeln. */
    public List<String> getPartsOfSpeach() {
        return partsOfSpeach;
    }

    /** Erstellt den ersten freien Namen mit dem übergebenen Anfang. */
    public String createNotExistingName(String nameStart) {
        String name = "MUSS ICH LEIDER INITIALISIEREN";
        int number = 0;
        boolean searchingForFreeName = true;

        while (searchingForFreeName) {
            ++number;
            name = nameStart + number;
            searchingForFreeName = isNameKnown(name);
        }

        return name;
    }

    /** Übergibt die Liste der eigenen Listen in einer neuen Sortierung. */
    public void setOrderAndStoreOwnLists(List<OwnList> ownListsInChangedOrder) {
        checkListIntegrity(ownListsInChangedOrder);
        ownLists.clear();
        ownLists.addAll(ownListsInChangedOrder);
        storeOwnLists();
    }

    private void checkListIntegrity(List<OwnList> ownListsInChangedOrder) {
        try {
            tryToCheckListIntegrity(ownListsInChangedOrder);
        }
        catch(Exception exception) {
            throw new RuntimeException("Es gab ein Problem beim aktualisieren der eigenen Listen "
                    + "nach Umsortierung: " + exception.getMessage());
        }
    }

    private void tryToCheckListIntegrity(List<OwnList> ownListsInChangedOrder) {
        CollectionsHelper.checkSizes(ownLists, ownListsInChangedOrder);
        CollectionsHelper.checkAllElementsOfFirstAreInSecond(ownLists, ownListsInChangedOrder);
        CollectionsHelper.checkAllElementsOfFirstAreInSecond(ownListsInChangedOrder, ownLists);
    }

    /** Gibt die Liste aller eigenen Listen zurück, die die übergebene Vokabel enthalten. */
    public List<OwnList> getOwnListsWithVocable(Vocable vocable) {
        List<OwnList> ownListsWithVocable = new ArrayList<>();

        for (OwnList ownList : ownLists) {
            if (ownList.getVocables().contains(vocable)) {
                ownListsWithVocable.add(ownList);
            }
        }

        return ownListsWithVocable;
    }

    /** Ermittelt die Namen aller Listen, in denen die Vokabel vorkommt. */
    public List<String> findNamesOfListsWithVocable(Vocable vocable) {
        List<String> names = new ArrayList<>();

        for (OwnList ownList : getOwnListsWithVocable(vocable)) {
            String name = ownList.getName();
            names.add(name);
        }

        return names;
    }

    /** Ermittelt alle eigenen Listen, die zur übergebenen Kategorie und Unterkategorie gehören. */
    public List<OwnList> findOwnListsWithCategoryAndSubCategory(String category,
            String subCategory) {
        List<OwnList> ownListsWithCategoryAndSubCategory = new ArrayList<>();

        for (OwnList ownList : ownLists) {
            if (category.equals(ownList.getCategory())
                    && subCategory.equals(ownList.getSubCategory())) {
                ownListsWithCategoryAndSubCategory.add(ownList);
            }
        }

        return ownListsWithCategoryAndSubCategory;
    }

    /** Sortiert die Liste mit den übergebenen eigenen Listen nach deren Namen. */
    public static void sortListsByName(List<OwnList> ownLists) {
        Collections.sort(ownLists, new Comparator<OwnList>() {
            @Override
            public int compare(OwnList list1, OwnList list2) {
                String name1 = list1.getName();
                String name2 = list2.getName();
                return name1.compareTo(name2);
            }
        });
    }

    /** Ermittelt, ob es eine eigene Liste mit den Daten aus dem Persistenzobjekt gibt. */
    public boolean containsOwnList(OwnListPersistanceData ownListData) {
        String category = ownListData.getCategory();
        String subCategory = ownListData.getSubCategory();
        String name = ownListData.getName();

        for (OwnList ownList : ownLists) {
            if (category.equals(ownList.getCategory())
                    && subCategory.equals(ownList.getSubCategory())
                    && name.equals(ownList.getName())) {
                return true;
            }
        }

        return false;
    }

//    /**
//     * Gibt die eigene Liste zurück, die den Daten aus dem Persistenzobjekt entspricht.
//     *
//     * Gibt es kein solches Objekt, wird eine Ausnahme geworfen.
//     *
//     * Es sollte vorher mit containsOwnList() geprüft werden, ob es eine solche gibt!
//     */
//    public OwnList getOwnList(OwListPersistanceData ownListData) {
//        String category = ownListData.getCategory();
//        String subCategory = ownListData.getSubCategory();
//        String name = ownListData.getName();
//
//        for (OwnList ownList : ownLists) {
//            if (category.equals(ownList.getCategory())
//                    && subCategory.equals(ownList.getSubCategory())
//                    && name.equals(ownList.getName())) {
//                return ownList;
//            }
//        }
//
//        throw new IllegalArgumentException("Es gibt keine Liste zu den Suchkriterien aus dem "
//                + "Persitenz-Objekt:\n"
//                + "\t" + "Kategorie     : '" + category + "'\n"
//                + "\t" + "Unterkategorie: '" + subCategory + "'\n"
//                + "\t" + "Name          : '" + name + "'\n"
//                );
//    }

    /**
     * Setzt in allen Bars mit den Vokabularien die Vordergrundfarbe auf den nach den Optionen und
     * ggf. dem Durchschnitt der erfolgreichen Abfrage der Vokabeln aus dem Vokabular richtig.
     */
    public void setCorrectForegroundColorOfVocabularyBarsInGui() {
        ownListInGuiRefresher.setCorrectForegroundColorOfVocabularyBarsLater();
    }

    /** Erstellt den Teil mit den eigenen Listen neu. */
    public void actualizeOwnListsPartInGui() {
        ownListInGuiRefresher.actualizeOwnListsPart();
    }

}
