package de.duehl.vocabulary.japanese.startup.logic.steps;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import de.duehl.basics.datetime.date.ImmutualDate;
import de.duehl.basics.datetime.time.watch.StopWatch;
import de.duehl.basics.io.FileHelper;
import de.duehl.basics.text.NumberString;
import de.duehl.basics.text.Text;
import de.duehl.vocabulary.japanese.common.data.InternalAdditionalVocableData;
import de.duehl.vocabulary.japanese.common.persistence.Options;
import de.duehl.vocabulary.japanese.data.Vocable;
import de.duehl.vocabulary.japanese.data.Vocabulary;
import de.duehl.vocabulary.japanese.logic.internal.InternalDataRequester;
import de.duehl.vocabulary.japanese.logic.internal.RealInternalDataRequester;
import de.duehl.vocabulary.japanese.startup.ui.data.SplashScreenable;
import de.duehl.vocabulary.japanese.tools.VocabularyTools;

import static de.duehl.vocabulary.japanese.startup.logic.StartupLoader.INTERNAL_VARIABLE_DATA_EXTENSION;
import static de.duehl.vocabulary.japanese.startup.logic.StartupLoader.INTERNAL_DATA_DIRECTORY;;

/**
 * Diese Klasse steht für den Schritt der die Vokabeln mit den internen Daten verbindet beim
 * Startup des Vokabeltrainers.
 *
 * Verbindet die Vokabeln mit den internen Daten.
 *
 * Dabei wird darauf auf die folgenden Umstände geachtet:
 *
 *     1) Ob dabei Schlüssel aus den eingelesenen internen Daten übrig bleiben, was darauf
 *        hindeutet, dass man an den Vokabeln etwas geändert hat und diese internen Daten
 *        entfernt werden sollten, ggf., nachdem man Daten daraus in die neu erzeugten
 *        internen Daten übernommen hat.
 *        Die erfolgt über die Liste
 *            unusedKeysFromReadInternalData.
 *     2) Es wird geprüft, dass keine zwei verschiedenen Vokabeln zum gleichen Schlüssel
 *        führen.
 *        Durch die Prüfung in
 *            VocabularyAndSoundFileFinder#
 *                checkIfVocablesAreUniqueWithKeyFromKanaAndFirstTranslations()
 *        wissen wir zwar, dass die Kombination von Kana und der ersten Übersetzung eindeutig
 *        ist. Auch der von
 *            VocabularyTools#createVocableKey()
 *        erzeugte Schlüssel basiert auf diesen beiden Daten, allerdings werden am Schlüssel
 *        einige Ersetzungen vorgenommen, so dass man nicht mehr sicher sein kann, dass der
 *        Schlüssel wirklich eindeutig ist.
 *        Also wird geprüft, dass es nicht vorkommt, dass die Schlüssel zweier
 *        unterschiedlicher Vokabeln dennoch gleich sind.
 *        Die erfolgt über die Listen
 *            seenKeysFromVocables und
 *            multipleCreatedKeysFromVocables.
 *
 * @version 1.01     2025-11-24
 * @author Christian Dühl
 */

public class Step05JoinVocablesAndInternalData extends StartupStep {

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

    /** Das Verzeichnis der internen Daten zu einer Vokabel nach ihrem Schlüssel. */
    private final Map<String, InternalAdditionalVocableData> key2InternalDataMap;

    /** Die Liste aller Schlüssel der eingelesenen internen Daten. */
    private final List<String> allKeysFromReadInternalData;

    /** Das Verzeichnis der internen Daten zu einer Vokabel nach der zugehörigen Variablen. */
    private Map<Vocable, InternalAdditionalVocableData> vocable2InternalDataMap;

    /** Zählt, wie viele interne Daten neu angelegt wurden. */
    private int newInternalDataCreationCounter;

    /**
     * Die Liste der Schlüssel von eingelesenen internen Daten, welche keiner Vokabel zugeordnet
     * werden konnten.
     */
    private List<String> unusedKeysFromReadInternalData;

    /** Die Liste der Schlüssel die aus einer Vokabel erzeugt wurden. */
    private List<String> seenKeysFromVocables;

    /** Die Liste der Schlüssel die identisch aus mehr als einer Vokabel erzeugt wurden. */
    private List<String> multipleCreatedKeysFromVocables;

    /** Das Objekt das zu Vokabeln die internen Daten abfragt. */
    private InternalDataRequester internalDataRequester;

    /**
     * Konstruktor.
     *
     * @param step
     *            Der Schritt der durchgeführt wird.
     * @param options
     *            Die Programmoptionen.
     * @param splashScreen
     *            Die grafische Oberfläche beim Start in der die Meldungen angezeigt werden.
     * @param watch
     *            Misst die Laufzeit des gesamten Startups.
     * @param vocabularies
     *            Die Liste mit den bekannten Vokabularien.
     * @param key2InternalDataMap
     *            Die Liste aller Schlüssel der eingelesenen internen Daten.
     * @param allKeysFromReadInternalData
     *            Das Objekt das zu Vokabeln die internen Daten abfragt.
     */
    public Step05JoinVocablesAndInternalData(String step, Options options,
            SplashScreenable splashScreen, StopWatch watch, List<Vocabulary> vocabularies,
            Map<String, InternalAdditionalVocableData> key2InternalDataMap,
            List<String> allKeysFromReadInternalData) {
        super(step, options, splashScreen, watch);
        this.vocabularies = vocabularies;
        this.key2InternalDataMap = key2InternalDataMap;
        this.allKeysFromReadInternalData = allKeysFromReadInternalData;
    }

    /** Führt den eigentlichen Inhalt des Schritts aus. */
    @Override
    protected void runInternalStep() {
        appendMessage("Verbinde die Vokabeln mit den internen benutzerabhängigen Daten ...");
        vocable2InternalDataMap = new HashMap<>();
        newInternalDataCreationCounter = 0;

        unusedKeysFromReadInternalData = new ArrayList<>();
        unusedKeysFromReadInternalData.addAll(allKeysFromReadInternalData);

        seenKeysFromVocables = new ArrayList<>();
        multipleCreatedKeysFromVocables = new ArrayList<>();

        for (Vocabulary vocabulary : vocabularies) {
            for (Vocable vocable : vocabulary.getVocables()) {
                joinVocableInternalData(vocable);
            }
        }

        appendMessage("Verzeichnis mit " + NumberString.taupu(vocable2InternalDataMap.size())
                + " Verweisen von Vokabeln auf interne Datensätze aufgebaut.");
        appendMessage(NumberString.taupu(newInternalDataCreationCounter)
                + " neue interne Daten angelegt.");

        if (unusedKeysFromReadInternalData.isEmpty()) {
            appendMessage("Alle interne Daten zu Vokabeln wurden einer Vokabel zugeordnet.");
        }
        else {
            List<String> warnings = new ArrayList<>();
            String interneDaten = NumberString.germanPlural(unusedKeysFromReadInternalData.size(),
                    "interne Daten", "internes Datum");
            String welche = NumberString.germanPlural(unusedKeysFromReadInternalData.size(),
                    "welche", "welches");
            warnings.add("Es gibt " + NumberString.taupu(unusedKeysFromReadInternalData.size())
                    + " " + interneDaten + " zu Vokabeln, " + welche
                    + " keiner Vokabel zugeordnet wurden:");
            for (String unusedKey : unusedKeysFromReadInternalData) {
                warnings.add("    " + unusedKey);
            }

            String warningsString = Text.joinWithLineBreak(warnings);
            appendMessage(warningsString);
            warningsInStep(warningsString);
        }

        if (multipleCreatedKeysFromVocables.isEmpty()) {
            appendMessage("Alle Vokabeln erzeugen unterschiedliche Schlüssel.");
            internalDataRequester = new RealInternalDataRequester(vocable2InternalDataMap);
        }
        else {
            List<String> errorLines = new ArrayList<>();
            errorLines.add("Es gibt " + NumberString.taupu(multipleCreatedKeysFromVocables.size())
                    + " Schlüssel, welche zu mehr als einer Vokabel gehören");
            for (String multipleCreatedKey : multipleCreatedKeysFromVocables) {
                errorLines.add("    " + multipleCreatedKey);
            }

            for (String line : errorLines) {
                appendMessage(line);
            }

            errorsInStep(Text.joinWithLineBreak(errorLines));
        }
    }

    /** Verbindet die übergebene Vokabel über den Schlüssel mit ihren internen Daten. */
    private void joinVocableInternalData(Vocable vocable) {
        String key = VocabularyTools.createVocableKey(vocable);
        unusedKeysFromReadInternalData.remove(key); // dieser interne Schlüssel wird verwendet!

        if (seenKeysFromVocables.contains(key)) {
            if (!multipleCreatedKeysFromVocables.contains(key)) {
                multipleCreatedKeysFromVocables.add(key);
            }
        }
        else {
            seenKeysFromVocables.add(key);
        }

        InternalAdditionalVocableData data;
        if (key2InternalDataMap.containsKey(key)) {
            data = key2InternalDataMap.get(key);
        }
        else {
            data = new InternalAdditionalVocableData();
            data.setKey(key);
            String internalDataFilename = FileHelper.concatPathes(INTERNAL_DATA_DIRECTORY,
                    key + INTERNAL_VARIABLE_DATA_EXTENSION);
            data.setFilename(internalDataFilename);
            data.setFirstSeenDate(new ImmutualDate()); // heute

            appendMessage("neu: " + internalDataFilename);
            ++newInternalDataCreationCounter;
            data.save();
        }

        vocable2InternalDataMap.put(vocable, data);
    }

    /**
     * Getter für das Verzeichnis der internen Daten zu einer Vokabel nach der zugehörigen
     * Variablen.
     */
    public Map<Vocable, InternalAdditionalVocableData> getVocable2InternalDataMap() {
        return vocable2InternalDataMap;
    }

    /** Getter für das Objekt, das zu Vokabeln die internen Daten abfragt. */
    public InternalDataRequester getInternalDataRequester() {
        return internalDataRequester;
    }

}
