package de.duehl.vocabulary.japanese.ui;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Image;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import de.duehl.basics.collections.CollectionsHelper;
import de.duehl.basics.datetime.DateAndTimeHelper;
import de.duehl.basics.datetime.date.ImmutualDate;
import de.duehl.basics.io.Charset;
import de.duehl.basics.io.FileHelper;
import de.duehl.basics.system.SystemTools;
import de.duehl.basics.text.NumberString;
import de.duehl.basics.text.Text;
import de.duehl.basics.text.html.HtmlTool;
import de.duehl.swing.logic.LongTimeProcessInformer;
import de.duehl.swing.ui.GuiTools;
import de.duehl.swing.ui.components.selections.FontSizeChangable;
import de.duehl.swing.ui.dialogs.base.NonModalFrameDialogBase;
import de.duehl.swing.ui.menu.collection.tools.MyMenuItemCollectionHelper;
import de.duehl.swing.ui.text.TextViewer;
import de.duehl.swing.ui.text.html.HtmlDialog;
import de.duehl.vocabulary.japanese.VocabularyTrainerVersion;
import de.duehl.vocabulary.japanese.common.chan.Chan;
import de.duehl.vocabulary.japanese.common.data.InternalAdditionalVocableData;
import de.duehl.vocabulary.japanese.common.data.TranslationDirection;
import de.duehl.vocabulary.japanese.common.data.VocabularySortOrder;
import de.duehl.vocabulary.japanese.common.persistence.Options;
import de.duehl.vocabulary.japanese.common.persistence.data.HistoricalOwnListPersistanceDataList;
import de.duehl.vocabulary.japanese.common.persistence.data.OwnListPersistanceData;
import de.duehl.vocabulary.japanese.common.ui.resources.IconDefinitions;
import de.duehl.vocabulary.japanese.data.FumikoDataStructures;
import de.duehl.vocabulary.japanese.data.KanjiSet;
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.data.symbol.Kanji;
import de.duehl.vocabulary.japanese.grammar.table.GrammarTableCreator;
import de.duehl.vocabulary.japanese.io.ManualVocabularySortOrderFileIo;
import de.duehl.vocabulary.japanese.logic.VocabularyTrainerLogic;
import de.duehl.vocabulary.japanese.logic.internal.InternalDataRequester;
import de.duehl.vocabulary.japanese.logic.ownlists.OwnLists;
import de.duehl.vocabulary.japanese.logic.sort.VocableSorter;
import de.duehl.vocabulary.japanese.logic.symbol.kana.html.HiraganaHtmlCreator;
import de.duehl.vocabulary.japanese.logic.symbol.kana.html.KatakanaHtmlCreator;
import de.duehl.vocabulary.japanese.logic.symbol.kana.test.HiraganaTesterLogic;
import de.duehl.vocabulary.japanese.logic.symbol.kana.test.KatakanaTesterLogic;
import de.duehl.vocabulary.japanese.logic.symbol.kanji.test.KanjiTestListFromGermanMeaningCreator;
import de.duehl.vocabulary.japanese.logic.symbol.kanji.test.KanjiTesterLogic;
import de.duehl.vocabulary.japanese.logic.symbol.kanji.unknown.UnknownCharactersInKanjiFieldOfVocablesDeterminer;
import de.duehl.vocabulary.japanese.logic.test.VocableListTesterLogic;
import de.duehl.vocabulary.japanese.statistics.VocabularyTrainerStatisticCreator;
import de.duehl.vocabulary.japanese.tools.VocableSubsetCreator;
import de.duehl.vocabulary.japanese.ui.components.TranslationDirectionGuiWithSwitchButton;
import de.duehl.vocabulary.japanese.ui.components.bars.VocabularyBar;
import de.duehl.vocabulary.japanese.ui.creation.menu.MenuCreation;
import de.duehl.vocabulary.japanese.ui.data.FumikoUiObjects;
import de.duehl.vocabulary.japanese.ui.data.OwnListInGuiRefresher;
import de.duehl.vocabulary.japanese.ui.dialog.AllVocablesWithSpecificConstraintDialog;
import de.duehl.vocabulary.japanese.ui.dialog.ComplexVocableSearchDialog;
import de.duehl.vocabulary.japanese.ui.dialog.kana.HiraganaTableDialog;
import de.duehl.vocabulary.japanese.ui.dialog.kana.KatakanaTableDialog;
import de.duehl.vocabulary.japanese.ui.dialog.kanji.kanjiset.KanjiSetManagementDialog;
import de.duehl.vocabulary.japanese.ui.dialog.kanji.kanjiset.check.KanjiSetIntegrityChecker;
import de.duehl.vocabulary.japanese.ui.dialog.kanji.table.KanjiTableDialog;
import de.duehl.vocabulary.japanese.ui.dialog.options.OptionsDialog;
import de.duehl.vocabulary.japanese.ui.dialog.ownlist.OwnListEditorDialog;
import de.duehl.vocabulary.japanese.ui.dialog.vocables.lister.VocabularyListerDialog;
import de.duehl.vocabulary.japanese.ui.dialog.vocables.sheet.VocableSheetsCreator;
import de.duehl.vocabulary.japanese.ui.dialog.vocables.sheet.VocabularySheetDialog;
import de.duehl.vocabulary.japanese.ui.dialog.vocables.single.SingleVocableVocabularyViewerDialog;
import de.duehl.vocabulary.japanese.ui.listcommander.OwnListCommander;
import de.duehl.vocabulary.japanese.ui.tabs.MainTabs;
import de.duehl.vocabulary.japanese.ui.tools.VocabularyTrainerUiTools;

import static de.duehl.vocabulary.japanese.common.persistence.SessionManager.VOCABLE_TRAINER_DIRECTORY;

/**
 * Diese Klasse stellt die grafische Oberfläche des Vokabel-Trainers dar.
 *
 * @version 1.01     2026-05-08
 * @author Christian Dühl
 */

public class VocabularyTrainerGui extends NonModalFrameDialogBase implements OwnListInGuiRefresher {

    private static final String DOCUMENTS = "de/duehl/vocabulary/japanese/documents";

    private static final String DIALOG_TITLE = Chan.CHAN_NAME + "s " +
            "Vokabel-Trainer";
            //"Vokabel-Trainer - Version " + VocabularyTrainerVersion.getOnlyVersion();

    private static final Image PROGRAM_IMAGE = IconDefinitions.loadProgramImage();


    /**
     * Die Anzahl an Vokabularien in einer Spalte, solange noch genug Platz ist.
     *
     * Wenn es mehr sein müssen, werden auch mehr dargestellt.
     */
    public static final int MAXIMAL_NUMBER_OF_VOCABULARY_PER_COLUMNN = 5;

    private static final boolean SHOW_TEST_ALL_BUTTON = false;


    /** Die Logik des Vokabel-Trainers. */
    private final VocabularyTrainerLogic logic;

    /** Die Statuszeile am unteren Ende. */
    private final JLabel statusBar;

    /** Liste der Elemente, deren Schriftgröße anpassbar sein soll. */
    private final List<FontSizeChangable> fontSizeChangingStringSelections;

    /** Liste der Panels mit Überschriften, deren Schriftgröße anpassbar sein soll. */
    private final List<JPanel> fontSizeChangingPanelsWithTitledBorders;

    /** Standardgröße für die Überschriften. */
    private int standardTitledBordersFontSize;

    /**
     * Die Schriftgröße der StringSelctions.
     *
     * Muss man in diesem Editor speichern, da sie für die einzelnen Guis der Umbrüche benötigt
     * wird.
     */
    private int stringSelectionFontSize;

    /**
     * Die Schriftgröße der StringSelctions.
     *
     * Muss man in diesem Editor speichern, da sie für die einzelnen Guis der Umbrüche benötigt
     * wird.
     */
    private int stringSelectionLabelFontSize;

    /**
     * Erzeugt den Panel mit den Reitern für die Wahl zwischen den Vokabularien und den eigenen
     * Listen.
     */
    private final MainTabs mainTabs;

    /**
     * Die Gui zur Wahl der Richtung der Übersetzung (Japanisch - Deutsch oder Deutsch -
     * Japanisch).
     */
    private final TranslationDirectionGuiWithSwitchButton translationDirectionGui;

    /** Der Suchbegriff, nachdem zuletzt gesucht wurde. */
    private String oldSearch;

    /** Der Suchbegriff für Kanji, nachdem zuletzt gesucht wurde. */
    private String oldKanjiSearch;

    /** Die Datenstrukturen des Vokabeltrainers. */
    private FumikoDataStructures dataStructures;

    /** Die häufig verwendeten Funktionen der grafischen Oberfläche des Vokabeltrainers. */
    private final FumikoUiObjects uiObjects;

    /**
     * Konstruktor.
     *
     * @param logic
     *            Die Logik des Vokabel-Trainers.
     * @param loadedOptions
     *            Die gerade geladenen Programmoptionen.
     */
    public VocabularyTrainerGui(VocabularyTrainerLogic logic, Options loadedOptions) {
        super(PROGRAM_IMAGE, DIALOG_TITLE);

        addClosingWindowListener(() -> logic.quit());
        addEscapeBehaviour();

        this.logic = logic;

        GuiTools.setNiceLayoutManager();

        uiObjects = new FumikoUiObjects(message -> setMessageLater(message), getWindowAsComponent(),
                (LongTimeProcessInformer) this, createErrorHandler(), () -> actualizeOwnListsPart(),
                () -> getLocation(), getProgramImage());

        mainTabs = new MainTabs(logic, this, uiObjects);
        statusBar = new JLabel(" ");

        fontSizeChangingStringSelections = CollectionsHelper.buildListFrom();
        fontSizeChangingPanelsWithTitledBorders = new ArrayList<>();

        translationDirectionGui = new TranslationDirectionGuiWithSwitchButton(loadedOptions, this);

        oldSearch = "";
        oldKanjiSearch = "";

        fillDialog();
    }

    @Override
    protected void populateDialog() {
        add(createMainPart(), BorderLayout.CENTER);
        add(createButtonPart(), BorderLayout.SOUTH);

        createMenu();
        storeTitledBorderStandardSize();
    }

    private Component createMainPart() {
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());

        panel.add(createUpperButtonPart(), BorderLayout.NORTH);
        panel.add(mainTabs.getPanel(), BorderLayout.CENTER);
        panel.add(createStatusPart(), BorderLayout.SOUTH);

        return panel;
    }

    private Component createUpperButtonPart() {
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());

        panel.add(createLeftUpperButtonPart(), BorderLayout.WEST);
        panel.add(createMiddleUpperButtonPart(), BorderLayout.CENTER);
        panel.add(createRightUpperButtonPart(), BorderLayout.EAST);

        return panel;
    }

    private Component createLeftUpperButtonPart() {
        JPanel panel = new JPanel();
        panel.setLayout(new FlowLayout(FlowLayout.LEFT, 5, 5));

        panel.add(translationDirectionGui.getPanel());
        panel.add(createImportOwnListsButton());
        panel.add(createExportButton());
        panel.add(createTest10RandomVocablesButton());
        if (SHOW_TEST_ALL_BUTTON) {
            panel.add(createTestAllVocablesButton());
        }
        panel.add(createListAllVocablesButton());

        return panel;
    }

    private Component createImportOwnListsButton() {
        JButton button = VocabularyTrainerUiTools.createPicturedButton(
                IconDefinitions.IMPORT_OWN_LIST, e -> importOwnList(),
                "Vokabelliste importieren");
        return button;
    }

    private Component createExportButton() {
        JButton button = VocabularyTrainerUiTools.createPicturedButton(
                IconDefinitions.NEW_OWN_LIST,
                e -> createNewOwnList(),
                "Eine neue Vokabelliste anlegen");
        return button;
    }

    /** Eigene Vokabellisten importieren. */
    public void importOwnList() {
        OwnLists ownLists = dataStructures.getOwnLists();
        ownLists.importOwnList();
        actualizeOwnListsPart();
    }

    private Component createTestAllVocablesButton() {
        JButton button = new JButton(buttonHtml("alle Vokabeln\nabfragen"));
        button.addActionListener(e -> testAllVocabularies());
        return button;
    }

    private String buttonHtml(String text) {
        String html = text;
        html = html.replace("\n", "<br>");
        html = "<center>" + html  + "</center>";
        html = "<html>" + html  + "</html>";
        return html;
    }

    private Component createTest10RandomVocablesButton() {
        JButton button = new JButton(buttonHtml("10 zufällige\nVokabeln abfragen"));
        button.addActionListener(e -> test10RandomVocables());
        return button;
    }

    private Component createListAllVocablesButton() {
        JButton button = new JButton(buttonHtml("Liste aller\nVokabeln anzeigen"));
        button.addActionListener(e -> showAllVocablesAsList());
        return button;
    }

    private Component createMiddleUpperButtonPart() {
        JPanel panel = new JPanel();
        panel.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 5));

        panel.add(createSelectAllVocabulariesInTabButton());
        panel.add(createDeselectedAllVocabulariesInTabButton());

        return GuiTools.centerHorizontal(panel);
    }

    private Component createSelectAllVocabulariesInTabButton() {
        JButton button = new JButton(buttonHtml("alle Vokabulabrien auf dieser\nSeite auswählen"));
        button.addActionListener(e -> selectAllVocabulariesInTab());
        return button;
    }

    private Component createDeselectedAllVocabulariesInTabButton() {
        JButton button = new JButton(buttonHtml("alle Vokabulabrien auf\ndieser Seite abwählen"));
        button.addActionListener(e -> deselectAllVocabulariesInTab());
        return button;
    }

    /** Alle Vokabularien im aktuellen Reiter auswählen. */
    public void selectAllVocabulariesInTab() {
        List<VocabularyBar> barsOnSelectedTab = determineBarsOfSelectedTab();
        for (VocabularyBar vocabularyBar : barsOnSelectedTab) {
            vocabularyBar.setSelected(true);
        }
    }

    /** Alle Vokabularien im aktuellen Reiter abwählen. */
    public void deselectAllVocabulariesInTab() {
        List<VocabularyBar> barsOnSelectedTab = determineBarsOfSelectedTab();
        for (VocabularyBar vocabularyBar : barsOnSelectedTab) {
            vocabularyBar.setSelected(false);
        }
    }

    private Component createRightUpperButtonPart() {
        JPanel panel = new JPanel();
        panel.setLayout(new FlowLayout(FlowLayout.RIGHT, 5, 5));

        panel.add(createListSelectedVocabulariesButton());
        panel.add(createTestSelectedVocabulariesButton());
        panel.add(createTest10RandomVocablesFromSelectedVocabulariesButton());

        return panel;
    }

    private Component createListSelectedVocabulariesButton() {
        JButton button = new JButton(buttonHtml("ausgewählte\nVokabularien anzeigen"));
        button.addActionListener(e -> listSelectedVocabularies());
        return button;
    }

    private Component createTestSelectedVocabulariesButton() {
        JButton button = new JButton(buttonHtml("ausgewählte\nVokabularien abfragen"));
        button.addActionListener(e -> testSelectedVocabularies());
        return button;
    }

    private Component createTest10RandomVocablesFromSelectedVocabulariesButton() {
        JButton button = new JButton(buttonHtml("10 zufällige Vokabeln aus den\n"
                + "ausgewählten Vokabularien abfragen"));
        button.addActionListener(e -> test10RandomVocablesFromSelectedVocabularies());
        return button;
    }

    private Component createStatusPart() {
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        GuiTools.createTitle("Status", panel);
        fontSizeChangingPanelsWithTitledBorders.add(panel);

        panel.add(statusBar, BorderLayout.CENTER);

        return panel;
    }

    private Component createButtonPart() {
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());

        panel.add(createQuitButton(), BorderLayout.EAST);

        return panel;
    }

    private Component createQuitButton() {
        JButton button = new JButton("Beenden");
        button.addActionListener(e -> logic.quit());
        return button;
    }

    /**
     * Gibt die Schriftgröße der StringSelections an oder -1, falls der Wert nicht verfügbar ist.
     */
    public int getStringSelectionFontSize() {
        if (!fontSizeChangingStringSelections.isEmpty()) {
            FontSizeChangable selection = fontSizeChangingStringSelections.get(0);
            return selection.getFontSize();
        }

        return -1;
    }

    /** Setzt die Schriftgröße der StringSelections. */
    public void setStringSelectionFontSize(int fontSize) {
        stringSelectionFontSize = fontSize;
        for (FontSizeChangable selection : fontSizeChangingStringSelections) {
            selection.setFontSize(stringSelectionFontSize);
        }
    }

    /**
     * Gibt die Schriftgröße der Überschriften der StringSelections an oder -1, falls der Wert
     * nicht verfügbar ist.
     */
    public int getStringSelectionLabelFontSize() {
        if (!fontSizeChangingStringSelections.isEmpty()) {
            FontSizeChangable selection = fontSizeChangingStringSelections.get(0);
            return selection.getLabelFontSize();
        }

        return -1;
    }

    /** Setzt die Schriftgröße der der Überschriften der StringSelections. */
    public void setStringSelectionLabelFontSize(int fontSize) {
        stringSelectionLabelFontSize = fontSize;
        for (FontSizeChangable selection : fontSizeChangingStringSelections) {
            selection.setLabelFontSize(stringSelectionLabelFontSize);
        }
        for (JPanel panel : fontSizeChangingPanelsWithTitledBorders) {
            GuiTools.setTitledBorderFontSize(panel, stringSelectionLabelFontSize);
        }
    }

    private void storeTitledBorderStandardSize() {
        if (fontSizeChangingPanelsWithTitledBorders.isEmpty()) {
            standardTitledBordersFontSize = -1;
        }
        else {
            JPanel panel = fontSizeChangingPanelsWithTitledBorders.get(0);
            standardTitledBordersFontSize = GuiTools.getTitledBorderFontSizeOfPanel(panel);
        }
    }

    /** Setzt die Nachricht etwas später. */
    public void setMessageLater(String message) {
        String showMessage;
        if (message.isEmpty()) {
            showMessage = " "; // sonst wird de Statusbar schmaler und die Gui verrutscht.
        }
        else {
            showMessage = message;
        }
        SwingUtilities.invokeLater(() -> statusBar.setText(showMessage));
    }

    /** Zeigt den Optionendialog an. */
    public void showOptionDialog() {
        OptionsDialog dialog = new OptionsDialog(dataStructures.getOptions(), getLocation(),
                uiObjects);
        dialog.populate();
        dialog.setVisible(true);
        if (dialog.wasApplied()) {
            runAfterChangingOptions();
        }
    }

    private void runAfterChangingOptions() {
        saveOptions(); // Sicherheitshalber, falls Absturz vor Beenden
        showVocabularyBarsInWantedOrder();
        setCorrectForegroundColorOfVocabularyBarsLater();
        setCorrectTextOfVocabularyBarsLater();
        showOrHidePercentInVocabularyBars();
    }

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

    /** Zeigt Informationen über das Programm an. */
    public void about() {
        GuiTools.showAbout(Chan.CHAN_NAME + "s " + "Vokabel-Trainer", "", // "den"
                //"Grafiken von Christian Musil", // TODO will er erstmal nicht
                new VocabularyTrainerVersion().getVersion(),
                new ImageIcon(getProgramImage()), getFrame());
    }

    /** Zeigt die Hilfe an. */
    public void help() {
        String doc = DOCUMENTS + "/help/hilfe.html";
        URL url = this.getClass().getClassLoader().getResource(doc);
        showHtmlViaUrl("Hilfe", url);
    }

    /** Zeigt die offenen Punkte an. */
    public void todo() {
        String doc = DOCUMENTS + "/todo.html";
        URL url = this.getClass().getClassLoader().getResource(doc);
        showHtmlViaUrl("Weiterentwicklung - Todos", url);
    }

    /** Zeigt die Änderungen an. */
    public void changes() {
        String doc = DOCUMENTS + "/changes.html";
        URL url = this.getClass().getClassLoader().getResource(doc);
        showHtmlViaUrl("Änderungen", url);
    }

    /** Gibt die Änderungen als String zurück. */
    public String getChanges() {
        String doc = DOCUMENTS + "/changes.html";
        return readResourceToText(doc);
    }

    /** Zeigt die Tastenbelegung des Programms an. */
    public void keyboardDescription() {
        String css = readFormatCssResource();

        MyMenuItemCollectionHelper.generateAndShowMyMenuItemsHtmlDescriptionDialog(
                "Tastaturbelegung im Umbruch-Editor",
                css, getProgramImage(), getLocation());
    }

    /** Zeigt die Tastenbelegung des Programms sortiert nach den Tastaturbelegung an. */
    public void keyboardDescriptionSortByKeys() {
        String css = readFormatCssResource();

        MyMenuItemCollectionHelper.generateAndShowMyMenuItemsHtmlDescriptionSortByKeysDialog(
                "Tastaturbelegung im Umbruch-Editor in Reihenfolge der Tasten",
                css, getProgramImage(), getLocation());
    }

    private String readFormatCssResource() {
        String doc = DOCUMENTS + "/format.css";
        return readResourceToText(doc);
    }

    private String readResourceToText(String doc) {
        URL url = this.getClass().getClassLoader().getResource(doc);
        return HtmlTool.createContentFromUrl(url);
    }

    /** Erhöht die Schriftgröße in den StringSelections. */
    public void increaseFontSize() {
        for (FontSizeChangable selection : fontSizeChangingStringSelections) {
            increaseFontSize(selection);
        }
    }

    private void increaseFontSize(FontSizeChangable selection) {
        selection.biggerText(1);
    }

    /** Verringert die Schriftgröße in den StringSelections. */
    public void decreaseFontSize() {
        for (FontSizeChangable selection : fontSizeChangingStringSelections) {
            decreaseFontSize(selection);
        }
    }

    private void decreaseFontSize(FontSizeChangable selection) {
        selection.biggerText(-1);
    }

    /** Setzt die Schriftgröße im HTML und in den StringSelections auf den Standardwert. */
    public void standardFontSize() {
        for (FontSizeChangable selection : fontSizeChangingStringSelections) {
            selection.setToStandardFontSize();
        }
    }

    /** Erhöht die Schriftgröße in den Überschriften der StringSelections. */
    public void increaseLabelFontSize() {
        for (FontSizeChangable selection : fontSizeChangingStringSelections) {
            increaseLabelFontSize(selection);
        }
        for (JPanel panel : fontSizeChangingPanelsWithTitledBorders) {
            increaseTitledBorderFontSize(panel);
        }
    }

    private void increaseLabelFontSize(FontSizeChangable selection) {
        selection.biggerLabelText(1);
    }

    private void increaseTitledBorderFontSize(JPanel panel) {
        GuiTools.biggerTitledBorderFontOfPanel(panel, 1);
    }

    /** Verringert die Schriftgröße in den Überschriften der StringSelections. */
    public void decreaseLabelFontSize() {
        for (FontSizeChangable selection : fontSizeChangingStringSelections) {
            decreaseLabelFontSize(selection);
        }
        for (JPanel panel : fontSizeChangingPanelsWithTitledBorders) {
            decreaseTitledBorderFontSize(panel);
        }
    }

    private void decreaseLabelFontSize(FontSizeChangable selection) {
        selection.biggerLabelText(-1);
    }

    private void decreaseTitledBorderFontSize(JPanel panel) {
        GuiTools.biggerTitledBorderFontOfPanel(panel, -1);
    }

    /** Setzt die Schriftgröße in den StringSelections auf den Standardwert. */
    public void standardLabelFontSize() {
        for (FontSizeChangable selection : fontSizeChangingStringSelections) {
            selection.setToStandardLabelFontSize();
        }
        for (JPanel panel : fontSizeChangingPanelsWithTitledBorders) {
            defaultTitledBorderFontSize(panel);
        }
    }

    private void defaultTitledBorderFontSize(JPanel panel) {
        if (standardTitledBordersFontSize != -1) {
            GuiTools.setTitledBorderFontSize(panel, standardTitledBordersFontSize);
        }
    }

    private void createMenu() {
        try {
            tryToCreateMenu();
        }
        catch (Exception exception) {
            logic.reallyQuit(); // das Programm beendet sich an der Stelle leider nicht
                                // automatisch. Ist nicht so schlimm...
            System.out.println("Abbruch wegen Fehler beim Menüaufbau! exception = " + exception);
            exception.printStackTrace();
            System.exit(1);
        }
    }

    private void tryToCreateMenu() {
        MenuCreation menuCreation = new MenuCreation(logic, this);
        menuCreation.addTo(getFrame());
    }

    /** Beendet das Programm. */
    public void quit() {
        closeDialog();
    }

    /**
     * Übergibt nach dem Start des Programms die Datenstrukturen aus der Logik.
     *
     * Zeigt zu Beginn des Programms die eingelesenen Vokabularien und eigenen Listen an.
     *
     * @param dataStructures
     *            Die Datenstrukturen des Vokabeltrainers.
     */
    public void initGuiWithDataStructure(FumikoDataStructures dataStructures) {
        this.dataStructures = dataStructures;
        initWithDataStructures();
    }

    private void initWithDataStructures() {
        OwnLists ownLists = dataStructures.getOwnLists();
        ownLists.setOwnListInGuiRefresher((OwnListInGuiRefresher) this);
        mainTabs.initWithDataStructures(dataStructures);
        showVocabularyBarsInWantedOrder();
        showTranslationDirectionOnBarButtons();
        pack();
    }

    /** Zeigt einen Dialog mit einer Liste aller Variablen an. */
    public void showAllVocablesAsList() {
        List<Vocable> allVocables = logic.collectVocablesOfAllVocabularies();
        listVocables(allVocables, "alle Vokabeln");
        setMessageLater("Alle Vokabeln wurden als Liste angezeigt.");
    }

    /**
     * Zeigt die übergebenen Vokabeln als Liste an.
     *
     * @param vocables
     *            Die anzuzeigenden Vokabeln.
     * @param description
     *            Die Beschreibung der anzuzeigenden Vokabeln.
     */
    private void listVocables(List<Vocable> vocables, String description) {
        VocabularyListerDialog dialog = new VocabularyListerDialog(vocables, description,
                dataStructures, uiObjects, getLocation(), uiObjects.getInformer());
        dialog.setVisible(true);
    }

    /** Zeigt einen Dialog mit einem Blatt mit allen Variablen an. */
    public void showAllVocablesAsSheet() {
        List<Vocable> allVocables = logic.collectVocablesOfAllVocabularies();
        sheetWithVocables(allVocables, "alle Vokabeln");
        setMessageLater("Alle Vokabeln wurden als Blatt angezeigt.");
    }

    /**
     * Öffnet die übergebenen Vokabeln als Vokabelblatt.
     *
     * @param vocables
     *            Die anzuzeigenden Vokabeln.
     * @param description
     *            Die Beschreibung der Menge.
     */
    public void sheetWithVocables(List<Vocable> vocables, String description) {
        startLongTimeProcess(
                "Öffne Blattdarstellung mit " + NumberString.taupu(vocables.size()) + " Vokabeln");
        new Thread(() -> listVocabularyAsSheetInOwnThread(vocables, description)).start();
    }

    private void listVocabularyAsSheetInOwnThread(List<Vocable> vocables, String description) {
        try {
            tryToListVocabularyAsSheetInOwnThread(vocables, description);
        }
        catch (Exception exception) {
            GuiTools.informUser("Hier ging etwas schief", "Bei der Erzeugng der Panel für die "
                    + "Blattdarstellung gab es ein Problem.");
            endLongTimeProcess();
        }
    }

    private void tryToListVocabularyAsSheetInOwnThread(List<Vocable> vocables, String description) {
        VocableSheetsCreator creator = new VocableSheetsCreator(vocables, dataStructures, uiObjects,
                (shuffledVocables, vocablePanes) -> listVocabularyAsSheetInEdt(shuffledVocables,
                        vocablePanes, description),
                getLocation());
        creator.create();
    }

    private void listVocabularyAsSheetInEdt(List<Vocable> shuffledVocables,
            List<Component> vocablePanes, String description) {
        try {
            tryToListVocabularyAsSheetInEdt(shuffledVocables, vocablePanes, description);
        }
        catch (Exception exception) {
            GuiTools.informUser("Hier ging etwas schief", "Bei der Anzeige der Blattdarstellung "
                    + "gab es ein Problem.");
            endLongTimeProcess();
        }
    }

    private void tryToListVocabularyAsSheetInEdt(List<Vocable> shuffledVocables,
            List<Component> vocablePanes, String description) {
        VocabularySheetDialog dialog = new VocabularySheetDialog(shuffledVocables, vocablePanes,
                description, dataStructures, uiObjects, uiObjects.getInformer(), getLocation());
        dialog.setVisible(true);
        dialog.requestFocus();
        endLongTimeProcess();
        setMessageLater(Text.firstCharToUpperCase(description) + " wurden als Blatt angezeigt.");
    }

    /** Fragt zehn zufällige Vokabeln ab. */
    public void test10RandomVocables() {
        List<Vocable> allVocables = logic.collectVocablesOfAllVocabularies();
        List<Vocable> randomVocables = createVocablesSublist(allVocables, 10);

        VocableListTesterLogic tester = new VocableListTesterLogic(randomVocables,
                "Zehn zufällige Vokabeln", logic, dataStructures, uiObjects);
        tester.test();

        setCorrectForegroundColorOfVocabularyBarsLater();
        setMessageLater("Zehn zufällige Vokabeln wurden abgefragt.");

        perhapsSaveAsList(randomVocables, "zehn zufällige Vokabeln");
    }

    private List<Vocable> createVocablesSublist(List<Vocable> vocables, int numberOfWantedVocables) {
        VocableSubsetCreator subsetCreator = new VocableSubsetCreator(vocables,
                numberOfWantedVocables, dataStructures);
        subsetCreator.create();
        return subsetCreator.getSubset();
    }

    /** Fragt alle Vokabeln ab. */
    public void testAllVocabularies() {
        List<Vocable> allVocables = logic.collectVocablesOfAllVocabularies();
        VocableListTesterLogic tester = new VocableListTesterLogic(allVocables, "alle Vokabeln",
                logic, dataStructures, uiObjects);
        tester.test();

        setCorrectForegroundColorOfVocabularyBarsLater();
        setMessageLater("Alle Vokabeln wurden abgefragt.");
    }

    /** Alle Vokabeln der ausgewählten Vokabularien anzeigen. */
    public void listSelectedVocabularies() {
        List<Vocable> vocables = collectVocablesOfSelectedVocabularies();
        if (!vocables.isEmpty()) {
            VocabularyListerDialog dialog = new VocabularyListerDialog(vocables,
                    "Vokabeln ausgewählter Vokabularien", dataStructures, uiObjects, getLocation(),
                    uiObjects.getInformer());
            dialog.setVisible(true);
            setMessageLater("Die Vokabeln ausgewählter Vokabularien wurden als Liste angezeigt.");
        }
    }

    /** Alle Vokabeln der ausgewählten Vokabularien abfragen. */
    public void testSelectedVocabularies() {
        List<Vocable> vocables = collectVocablesOfSelectedVocabularies();
        if (!vocables.isEmpty()) {
            VocableListTesterLogic tester = new VocableListTesterLogic(vocables,
                    "Vokabeln ausgewählter Vokabularien", logic, dataStructures, uiObjects);
            tester.test();

            setCorrectForegroundColorOfVocabularyBarsLater();
            setMessageLater("Die ausgewählten Vokabularien wurden abgefragt.");
        }
    }

    private List<Vocable> collectVocablesOfSelectedVocabularies() {
        List<Vocable> vocables = mainTabs.collectVocablesOfSelectedVocabularies();
        if (vocables.isEmpty()) {
            GuiTools.informUser(getWindowAsComponent(), "Keine Vokabularien ausgewählt",
                    "Es wurden leider keine Vokabularien ausgewählt.");
        }
        return vocables;
    }

    /** Zehn zufällige Vokabeln der ausgewählten Vokabularien anzeigen. */
    public void test10RandomVocablesFromSelectedVocabularies() {
        List<Vocable> vocables = collectVocablesOfSelectedVocabularies();
        if (!vocables.isEmpty()) {
            List<Vocable> randomVocables = createVocablesSublist(vocables, 10);

            VocableListTesterLogic tester = new VocableListTesterLogic(randomVocables,
                    "Zehn zufällige Vokabeln aus den ausgewählten Vokabularien", logic,
                    dataStructures, uiObjects);
            tester.test();

            setCorrectForegroundColorOfVocabularyBarsLater();
            setMessageLater(
                    "Zehn zufällige Vokabeln aus den ausgewählten Vokabularien wurden abgefragt.");

            perhapsSaveAsList(randomVocables, "zehn zufällige Vokabeln");
        }
    }

    /** Setzt die Sortierung der Vokabularien auf die übergeben Reihenfolge fest. */
    public void setVocabularySortOrder(VocabularySortOrder sortOrder) {
        logic.setVocabularySortOrder(sortOrder);
        showVocabularyBarsInWantedOrder();
    }

    /** Sortiert die angezeigten Vokabularien so, wie es in den Optionen eingetragen ist. */
    private void showVocabularyBarsInWantedOrder() {
        mainTabs.showVocabularyBarsInWantedOrder();
        setMessageLater(" ");
        refresh();
    }

    /** Schaltet den Modus zum individuellen Anordnen der angezeigten Vokabularien an oder aus. */
    public void toggleManualVocabularySortMode() {
        VocabularySortOrder sortOrder = dataStructures.getOptions().getVocabularySortOrder();
        if (sortOrder == VocabularySortOrder.MANUAL_SORT_ORDER) {
            mainTabs.showOrHideIndividualVocabularySortModeMoveButtons();
            pack();
            revalidate();
            setMessageLater(" ");
        }
        else {
            GuiTools.informUser(getWindowAsComponent(), "Falscher Sortiermodus", ""
                    + "Das Anzeigen oder Verstecken der Buttons zum Bewegen der Bars funktioniert "
                    + "nur im individuellen Sortiermodus.");
        }
    }

    /** Ermittelt die auf dem aktuellen Reiter angezeigten VocabularyBars. */
    private List<VocabularyBar> determineBarsOfSelectedTab() {
        return mainTabs.determineBarsOfSelectedTab();
    }

    /**
     * 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.
     */
    @Override
    public void setCorrectForegroundColorOfVocabularyBarsLater() {
        SwingUtilities.invokeLater(() -> setCorrectForegroundColorOfVocabularyBars());
    }

    /**
     * Setzt die Vordergrundfarbe auf den nach den Optionen und ggf. dem Durchschnitt der
     * erfolgreichen Abfrage der Vokabeln aus dem Vokabular in allen Vokabularien richtig.
     */
    private void setCorrectForegroundColorOfVocabularyBars() {
        mainTabs.setCorrectForegroundColorOfVocabularyBars();
        repaint(); // reicht alleine nicht aus.
        revalidate();
    }

    /**
     * Aktualisiert in allen Bars mit den Vokabularien den angezeigten Text je nach den Optionen.
     */
    private void setCorrectTextOfVocabularyBarsLater() {
        SwingUtilities.invokeLater(() -> setCorrectTextOfVocabularyBars());
    }

    private void setCorrectTextOfVocabularyBars() {
        mainTabs.setCorrectTextOfVocabularyBars();
        repaintAndPack();
    }

    private void repaintAndPack() {
        repaint(); // reicht alleine nicht aus.
        revalidate();

        pack();
    }

    /**
     * Aktualisiert in allen Bars mit den Vokabularien den angezeigten Text zum Prozent des
     * Erfolges je nach den Optionen.
     */
    private void showOrHidePercentInVocabularyBars() {
        SwingUtilities.invokeLater(() -> showOrHidePercentInVocabularyBarsLater());
    }

    private void showOrHidePercentInVocabularyBarsLater() {
        mainTabs.showOrHidePercentInVocabularyBarsLater();
        repaintAndPack();
    }

    /**
     * Speichert die Beschreibungen der Vokabularien in die benutzerspezifische Datei.
     *
     * Die sammle ich hier hierarchisch ein und speichere dann.
     *
     * Die Reihenfolge der Listen wird nicht hier mit gespeichert.
     * Die könnten auch zufällig genauso wie ein Vokabularium heißen.
     *
     * Wird nur aufgerufen, wenn die Vokabularien auch in der individuellen Sortierung angezeigt
     * werden.
     */
    public void saveManualVocabularyOrder() {
        VocabularySortOrder sortOrder = dataStructures.getOptions().getVocabularySortOrder();
        if (sortOrder == VocabularySortOrder.MANUAL_SORT_ORDER) {
            List<Vocabulary> vocabularies = mainTabs.getVocabulariesInIndividualOrder();
            ManualVocabularySortOrderFileIo.saveManualVocabularyOrder(vocabularies);

            List<OwnList> ownListList = mainTabs.getOwnListsInIndividualOrder();
            OwnLists ownLists = dataStructures.getOwnLists();
            /*
             * Bei Fehlern bei Start ist mainTabs noch null, dies wird aber von reallyQuit()
             * aufgerufen, daher prüfe ich:
             */
            if (null != ownLists) {
                ownLists.setOrderAndStoreOwnLists(ownListList);
            }
        }
    }

    /**
     * Hier werden die persistent gespeicherten zuletzt aktiven Tabs sowohl bei den Vokabularien
     * als auch bei den eigenen Listen wieder angezeigt.
     *
     * Das Gegenstück zu dieser Methode ist storeShownTabIndices().
     */
    public void showTabsWeViewedLastTime() {
        mainTabs.showTabsWeViewedLastTime();
    }

    /** Speichert die Indices des aktuell angezeigten Tabs in jeder Kategorie in den Optionen. */
    public void storeShownTabIndices() {
        Options options = dataStructures.getOptions();
        mainTabs.storeShownTabIndices(options);
    }

    /** Vokabeln suchen (einfach). */
    public void vocableSearch() {
        String search = GuiTools.askUserToEnterAStringValue(getWindowAsComponent(),
                "Bitte den Suchbegriff eigeben", ""
                        + "Bitte geben Sie den Begrif ein, nach dem in den Vokabeln in "
                        + "japanischer\n"
                        + "Form, deutscher Form sowie in den Bemerkungen, den Suchbegriffen und "
                        + "Wortarten\n"
                        + "gesucht werden soll.",
                        oldSearch);
        if (!search.isBlank()) {
            search = search.strip();
            oldSearch = search;
            List<Vocable> allVocables = logic.collectVocablesOfAllVocabularies();
            List<Vocable> foundVocables = new ArrayList<>();
            for (Vocable vocable : allVocables) {
                if (vocable.contains(search)) {
                    foundVocables.add(vocable);
                }
            }
            if (foundVocables.isEmpty()) {
                String message = "Es gibt keine Vokabeln mit dem Suchbegriff '" + search + "'.";
                GuiTools.informUser(getWindowAsComponent(), "Hinweis", message);
                setMessageLater(message);
            }
            else {
                VocableSorter sorter = new VocableSorter(foundVocables, search);
                sorter.sort();

                SingleVocableVocabularyViewerDialog dialog = new SingleVocableVocabularyViewerDialog(
                        foundVocables, "Vokabeln mit dem Suchbegriff '" + search + "'",
                        dataStructures, uiObjects);
                dialog.setOpenedFromSimpleSearch();
                dialog.setVisible(true);
                setMessageLater(
                        "Die Vokabeln mit dem Suchbegriff '" + search + "' wurden angezeigt.");
            }
        }
    }

    /** Vokabeln suchen (erweitert). */
    public void complexVocableSearch() {
        ComplexVocableSearchDialog dialog = new ComplexVocableSearchDialog(logic, this,
                dataStructures, uiObjects);
        dialog.setVisible(true);
    }

    /** Noch nicht abgefragte Vokabeln anzeigen. */
    public void showNotTestedVocables() {
        List<Vocable> notTestedVocables = determineNotTestedVocables();
        String noVocablesFoundMessage = "noch gar nicht abgefragt wurden";
        String noVocablesFoundTitle = "Noch nicht abgefragte Vokabeln";

        showVocables(notTestedVocables, noVocablesFoundMessage, noVocablesFoundTitle);
    }

    /** Noch nicht abgefragte Vokabeln als Blatt anzeigen. */
    public void showNotTestedVocablesAsSheet() {
        List<Vocable> notTestedVocables = determineNotTestedVocables();
        sheetWithVocables(notTestedVocables, "noch nicht abgefragte Vokabeln");
        setMessageLater("Noch nicht abgefragte Vokabeln wurden als Blatt angezeigt.");
    }

    /** Noch nicht abgefragte Vokabeln als Liste anzeigen. */
    public void showNotTestedVocablesAsList() {
        List<Vocable> notTestedVocables = determineNotTestedVocables();
        listVocables(notTestedVocables, "noch nicht abgefragte Vokabeln");
        setMessageLater("Noch nicht abgefragte Vokabeln wurden als Liste angezeigt.");
    }

    /** Noch nicht abgefragte Vokabeln abfragen. */
    public void testNotTestedVocables() {
        List<Vocable> notTestedVocables = determineNotTestedVocables();
        String noVocablesFoundMessage = "noch gar nicht abgefragt wurden";
        String noVocablesFoundTitle = "Noch nicht abgefragte Vokabeln";

        testVocables(notTestedVocables, noVocablesFoundMessage, noVocablesFoundTitle);

        perhapsSaveAsList(notTestedVocables, noVocablesFoundMessage);
    }

    private List<Vocable> determineNotTestedVocables() {
        List<Vocable> notTestedVocables = new ArrayList<>();

        for (Vocable vocable : logic.collectVocablesOfAllVocabularies()) {
            InternalDataRequester requester = dataStructures.getInternalDataRequester();
            InternalAdditionalVocableData data = requester.getInternalDataForVocable(vocable);
            int testCount = getTestCount(data);
            if (testCount == 0) {
                notTestedVocables.add(vocable);
            }
        }

        return notTestedVocables;
    }

    private int getTestCount(InternalAdditionalVocableData data) {
        Options options = dataStructures.getOptions();
        TranslationDirection translationDirection = options.getTranslationDirection();
        if (translationDirection == TranslationDirection.JAPANESE_TO_GERMAN) {
            return data.getJapaneseToGermanTestCount();
        }
        else if (translationDirection == TranslationDirection.GERMAN_TO_JAPANESE) {
            return data.getGermanToJapaneseTestCount();
        }
        else {
            throw new RuntimeException("Unbekannte Übersetzungsrichtung " + translationDirection);
        }
    }

    /** Noch nicht korrekt übersetzte Vokabeln anzeigen. */
    public void showNotCorrectTestedVocables() {
        List<Vocable> notCorrectTestedVocables = determineNotCorrectTestedVocables();
        String noVocablesFoundMessage = "noch nicht korrekt übersetzt wurden";
        String noVocablesFoundTitle = "Noch nicht korrekt übersetzte Vokabeln";

        showVocables(notCorrectTestedVocables, noVocablesFoundMessage, noVocablesFoundTitle);
    }

    /** Noch nicht korrekt übersetzte Vokabeln abfragen. */
    public void testNotCorrectTestedVocables() {
        List<Vocable> notCorrectTestedVocables = determineNotCorrectTestedVocables();
        String noVocablesFoundMessage = "noch nicht korrekt übersetzt wurden";
        String noVocablesFoundTitle = "Noch nicht korrekt übersetzte Vokabeln";

        testVocables(notCorrectTestedVocables, noVocablesFoundMessage, noVocablesFoundTitle);

        perhapsSaveAsList(notCorrectTestedVocables, noVocablesFoundMessage);
    }

    private List<Vocable> determineNotCorrectTestedVocables() {
        List<Vocable> notCorrectTestedVocables = new ArrayList<>();

        for (Vocable vocable : logic.collectVocablesOfAllVocabularies()) {
            InternalDataRequester requester = dataStructures.getInternalDataRequester();
            InternalAdditionalVocableData data = requester.getInternalDataForVocable(vocable);
            int correctTestCount = getCorrectTestCount(data);
            if (correctTestCount == 0) {
                notCorrectTestedVocables.add(vocable);
            }
        }

        return notCorrectTestedVocables;
    }

    private int getCorrectTestCount(InternalAdditionalVocableData data) {
        Options options = dataStructures.getOptions();
        TranslationDirection translationDirection = options.getTranslationDirection();
        if (translationDirection == TranslationDirection.JAPANESE_TO_GERMAN) {
            return data.getCorrectJapaneseToGermanTestCount();
        }
        else if (translationDirection == TranslationDirection.GERMAN_TO_JAPANESE) {
            return data.getCorrectGermanToJapaneseTestCount();
        }
        else {
            throw new RuntimeException("Unbekannte Übersetzungsrichtung " + translationDirection);
        }
    }

    /** Lange nicht mehr abgefragte Vokabeln anzeigen */
    public void showNotTestedForAWhileVocables() {
        List<Vocable> notTestedVocables = determineNotTestedForAWhileVocables();
        String noVocablesFoundMessage = "lange nicht mehr abgefragt wurden";
        String noVocablesFoundTitle = "Lange nicht mehr abgefragte Vokabeln";

        showVocables(notTestedVocables, noVocablesFoundMessage, noVocablesFoundTitle);
    }

    /** Lange nicht mehr abgefragte Vokabeln abfragen */
    public void testNotTestedForAWhileVocables() {
        List<Vocable> notTestedForAWhileVocables = determineNotTestedForAWhileVocables();
        String noVocablesFoundMessage = "lange nicht mehr abgefragt wurden";
        String noVocablesFoundTitle = "Lange nicht mehr abgefragte Vokabeln";

        testVocables(notTestedForAWhileVocables, noVocablesFoundMessage, noVocablesFoundTitle);

        perhapsSaveAsList(notTestedForAWhileVocables, noVocablesFoundMessage);
    }

    private List<Vocable> determineNotTestedForAWhileVocables() {
        List<Vocable> notTestedForAWhileVocables = new ArrayList<>();

        for (Vocable vocable : logic.collectVocablesOfAllVocabularies()) {
            InternalDataRequester requester = dataStructures.getInternalDataRequester();
            InternalAdditionalVocableData data = requester.getInternalDataForVocable(vocable);
            ImmutualDate lastDate = getLastTestDate(data);
            if (lastDateIsAWhileAgo(lastDate)) {
                notTestedForAWhileVocables.add(vocable);
            }
        }

        return notTestedForAWhileVocables;
    }

    private ImmutualDate getLastTestDate(InternalAdditionalVocableData data) {
        Options options = dataStructures.getOptions();
        TranslationDirection translationDirection = options.getTranslationDirection();
        if (translationDirection == TranslationDirection.JAPANESE_TO_GERMAN) {
            return data.getLastJapaneseToGermanTestDate();
        }
        else if (translationDirection == TranslationDirection.GERMAN_TO_JAPANESE) {
            return data.getLastGermanToJapaneseTestDate();
        }
        else {
            throw new RuntimeException("Unbekannte Übersetzungsrichtung " + translationDirection);
        }
    }

    /** Lange nicht mehr korrekt übersetzte Vokabeln anzeigen */
    public void showNotCorrectTestedForAWhileVocables() {
        List<Vocable> notCorrectTestedForAWhileVocables =
                determineNotCorrectTestedForAWhileVocables();
        String noVocablesFoundMessage = "lange nicht mehr korrekt übersetzt wurden";
        String noVocablesFoundTitle = "Lange nicht mehr korrekt übersetzt Vokabeln";

        showVocables(notCorrectTestedForAWhileVocables, noVocablesFoundMessage,
                noVocablesFoundTitle);
    }

    /** Lange nicht mehr korrekt übersetzte Vokabeln abfragen */
    public void testNotCorrectTestedForAWhileVocables() {
        List<Vocable> notCorrectTestedForAWhileVocables =
                determineNotCorrectTestedForAWhileVocables();
        String noVocablesFoundMessage = "lange nicht mehr korrekt übersetzt wurden";
        String noVocablesFoundTitle = "Lange nicht mehr korrekt übersetzt Vokabeln";

        testVocables(notCorrectTestedForAWhileVocables, noVocablesFoundMessage,
                noVocablesFoundTitle);

        perhapsSaveAsList(notCorrectTestedForAWhileVocables, noVocablesFoundMessage);
    }

    private List<Vocable> determineNotCorrectTestedForAWhileVocables() {
        List<Vocable> notCorrectTestedForAWhileVocables = new ArrayList<>();

        for (Vocable vocable : logic.collectVocablesOfAllVocabularies()) {
            InternalDataRequester requester = dataStructures.getInternalDataRequester();
            InternalAdditionalVocableData data = requester.getInternalDataForVocable(vocable);
            ImmutualDate lastCorrectDate = getLastCorrectTestDate(data);
            if (lastDateIsAWhileAgo(lastCorrectDate)) {
                notCorrectTestedForAWhileVocables.add(vocable);
            }
        }

        return notCorrectTestedForAWhileVocables;
    }

    private ImmutualDate getLastCorrectTestDate(InternalAdditionalVocableData data) {
        Options options = dataStructures.getOptions();
        TranslationDirection translationDirection = options.getTranslationDirection();
        if (translationDirection == TranslationDirection.JAPANESE_TO_GERMAN) {
            return data.getLastCorrectJapaneseToGermanTestDate();
        }
        else if (translationDirection == TranslationDirection.GERMAN_TO_JAPANESE) {
            return data.getLastCorrectGermanToJapaneseTestDate();
        }
        else {
            throw new RuntimeException("Unbekannte Übersetzungsrichtung " + translationDirection);
        }
    }

    private boolean lastDateIsAWhileAgo(ImmutualDate lastDate) {
        ImmutualDate now = new ImmutualDate();
        int dayDistance = lastDate.difference(now);
        Options options = dataStructures.getOptions();
        return dayDistance >= options.getNumberOfDaysForAWhile();
    }

    /** Neuste Vokabeln anzeigen. */
    public void showNewestVocables() {
        List<Vocable> newestVocables = determineNewestVocables();
        String noVocablesFoundMessage = "neu sind";
        String noVocablesFoundTitle = "Lange nicht mehr korrekt übersetzt Vokabeln";

        showVocables(newestVocables, noVocablesFoundMessage, noVocablesFoundTitle);
    }

    /** Neuste Vokabeln als Blatt anzeigen. */
    public void showNewestVocablesAsSheet() {
        List<Vocable> notTestedVocables = determineNewestVocables();
        sheetWithVocables(notTestedVocables, "neuste Vokabeln");
        setMessageLater("Neuste Vokabeln wurden als Blatt angezeigt.");
    }

    /** Neuste Vokabeln als Liste anzeigen. */
    public void showNewestVocablesAsList() {
        List<Vocable> notTestedVocables = determineNewestVocables();
        listVocables(notTestedVocables, "neuste Vokabeln");
        setMessageLater("Neuste Vokabeln wurden als Liste angezeigt.");
    }

    /** Neuste Vokabeln abfragen. */
    public void testNewestVocables() {
        List<Vocable> newestVocables = determineNewestVocables();
        String noVocablesFoundMessage = "neuste Vokabeln";
        String noVocablesFoundTitle = "Neuste Vokabeln";

        testVocables(newestVocables, noVocablesFoundMessage, noVocablesFoundTitle);

        perhapsSaveAsList(newestVocables, noVocablesFoundMessage);
    }

    private void perhapsSaveAsList(List<Vocable> vocables, String vocablesDescription) {
        if (!vocables.isEmpty()) { // nicht abgefragte Vokabeln ...
            //askUser
            boolean save = GuiTools.askUserDefaultNo(getWindowAsComponent(),
                    "Abgefragte Liste speichern?",
                    "Wollen sie die Liste mit den gerade abgefragten Vokabeln ("
                            + vocablesDescription + ") als eigene Vokabelliste speichern?");
            if (save) {
                saveAsList(vocables, vocablesDescription);
            }
        }
    }

    /**
     * Speichert die übergebenen Vokabeln mit dem übergebenen Namen als eigene Liste.
     *
     * @param vocables
     *            Die Vokabeln, die als Liste abgespeichert werden sollen.
     * @param name
     *            Der Name der Liste, dieser wird noch um das aktuelle Datum und Uhrzeit ergänzt.
     */
    public void saveAsList(List<Vocable> vocables, String name) {
        startLongTimeProcess("Liste wird gespeichert");
        new Thread(() -> saveAsListInOwnThread(vocables, name)).start();
    }

    private void saveAsListInOwnThread(List<Vocable> vocables, String name) {
        String nameSuggestion = name + " (" + DateAndTimeHelper.now() + ")";
        OwnLists ownLists = dataStructures.getOwnLists();
        ownLists.saveAsList(vocables, nameSuggestion);
        SwingUtilities.invokeLater(() -> saveAsListInEdt());
    }

    private void saveAsListInEdt() {
        endLongTimeProcess();
        actualizeOwnListsPart(); // Das hat seine eigene GlasPane, daher danach!
    }

    private List<Vocable> determineNewestVocables() {
        List<Vocable> notCorrectTestedForAWhileVocables = new ArrayList<>();

        for (Vocable vocable : logic.collectVocablesOfAllVocabularies()) {
            InternalDataRequester requester = dataStructures.getInternalDataRequester();
            InternalAdditionalVocableData data = requester.getInternalDataForVocable(vocable);
            ImmutualDate firstSeenDate = data.getFirstSeenDate();
            if (firstSeenIsNew(firstSeenDate)) {
                notCorrectTestedForAWhileVocables.add(vocable);
            }
        }

        return notCorrectTestedForAWhileVocables;
    }

    private boolean firstSeenIsNew(ImmutualDate firstSeenDate) {
        ImmutualDate now = new ImmutualDate();
        int dayDistance = firstSeenDate.difference(now);
        Options options = dataStructures.getOptions();
        return dayDistance <= options.getNumberOfDaysAVocableIsNew();
    }

    private void showVocables(List<Vocable> vocables, String noVocablesFoundMessage,
            String vocablesTitle) {
        if (vocables.isEmpty()) {
            GuiTools.informUser(getWindowAsComponent(), "Hinweis",
                    "Es gibt keine Vokabeln, die " + noVocablesFoundMessage + ".");
        }
        else {
            SingleVocableVocabularyViewerDialog dialog = new SingleVocableVocabularyViewerDialog(
                    vocables, vocablesTitle, dataStructures, uiObjects);
            dialog.setVisible(true);
        }
        setMessageLater("Angezeigt wurden: " + vocablesTitle);
    }

    private void testVocables(List<Vocable> vocables, String noVocablesFoundMessage,
            String vocablesTitle) {
        if (vocables.isEmpty()) {
            GuiTools.informUser(getWindowAsComponent(), "Hinweis",
                    "Es gibt keine Vokabeln, die " + noVocablesFoundMessage + ".");
        }
        else {
            VocableListTesterLogic tester = new VocableListTesterLogic(vocables, vocablesTitle,
                    logic, dataStructures, uiObjects);
            tester.test();

            setCorrectForegroundColorOfVocabularyBarsLater();
            setMessageLater("Getestet wurden: " + vocablesTitle);
        }
    }

    /** Zeigt die Statistik an. */
    public void showStatstics() {
        startLongTimeProcess("Erstelle Statistik");
        new Thread(() -> createStatsticsInOwnThread()).start();
    }

    private void createStatsticsInOwnThread() {
        VocabularyTrainerStatisticCreator creater = new VocabularyTrainerStatisticCreator(
                dataStructures, uiObjects);
        String html = creater.createStatisticReport();
        /*
         * Da Links nur funktionieren, wenn man das HTML als Url und nicht als Text setzt, muss ich
         * die Datei abspeichern und dann über den Dateinamen als URL laden.
         */
        String statisticFilename = saveStatistic(html);

        SwingUtilities.invokeLater(() -> showStatsticsInEdt(statisticFilename));
    }

    private String saveStatistic(String html) {
        String statisticFilename = FileHelper.concatPathes(VOCABLE_TRAINER_DIRECTORY,
                "statistik.html");
        FileHelper.writeTextToFile(html, statisticFilename, Charset.UTF_8);
        return statisticFilename;
    }

    private void showStatsticsInEdt(String statisticFilename) {
        endLongTimeProcess();

        URL url = createUrl(statisticFilename);
        if (null != url) {
            HtmlDialog dialog = new HtmlDialog("Statsitik", getProgramImage(), getLocation());
            dialog.showHtml(url);
            dialog.scrollScrollbarToMinimumLater();
            dialog.setVisible(true);
        }
    }

    private URL createUrl(String statisticFilename) {
        try {
            return tryToCreateUrl(statisticFilename);
        }
        catch (MalformedURLException exception) {
            createErrorHandler().error("Beim Wandeln der Datei mit der gespeicheren Statistik aus "
                    + "einer URI in eine URL trat ein Fehler auf.", exception);
            return null;
        }
    }

    private URL tryToCreateUrl(String statisticFilename) throws MalformedURLException {
        File file = new File(statisticFilename);
        URI uri = file.toURI();
        URL url = uri.toURL();
        return url;
    }

    /**
     * Verändert ob die Darstellung der angezeigten Vokabularien abhängig vom Erfolg bei den
     * letzten zehn Abfragen eingefärbt werden soll.
     */
    public void toggleColorVocabularyDependingOnLastSuccess() {
        Options options = dataStructures.getOptions();

        boolean colorVocabularyDependingOnLastSuccess =
                options.isColorVocabularyDependingOnLastSuccess();
        colorVocabularyDependingOnLastSuccess = !colorVocabularyDependingOnLastSuccess;
        options.setColorVocabularyDependingOnLastSuccess(colorVocabularyDependingOnLastSuccess);

        runAfterChangingOptions();
    }

    /**
     * Verändert ob der Erfolg bei den letzten zehn Abfragen der Vokabeln eines Vokabulars als
     * Prozentzahl mit angezeigt werden soll.
     */
    public void toggleShowSuccessPercentInVocabularyBar() {
        Options options = dataStructures.getOptions();

        boolean showSuccessPercentInVocabularyBar = options.isShowSuccessPercentInVocabularyBar();
        showSuccessPercentInVocabularyBar = !showSuccessPercentInVocabularyBar;
        options.setShowSuccessPercentInVocabularyBar(showSuccessPercentInVocabularyBar);

        runAfterChangingOptions();
    }

    /**
     * Verändert ob der Anfang der Beschreibung der Vokabularien (\"B1_K4_1_\" oder \"X_\")
     * ausgeblendet werden soll.
     */
    public void toggleHideStartOfVocabularyDescription() {
        Options options = dataStructures.getOptions();

        boolean hideStartOfVocabularyDescription = options.isHideStartOfVocabularyDescription();
        hideStartOfVocabularyDescription = !hideStartOfVocabularyDescription;
        options.setHideStartOfVocabularyDescription(hideStartOfVocabularyDescription);

        runAfterChangingOptions();
    }

    /** Erstellt den Teil mit den eigenen Listen neu. */
    @Override
    public void actualizeOwnListsPart() {
        new Thread(() -> actualizeOwnListsPartInOwnThread()).start();
    }

    private void actualizeOwnListsPartInOwnThread() {
        startLongTimeProcess("Baue die Anzeige der eigenen Listen neu auf");
        storeShownTabIndices(); // sonst ist man nach actualizeOwnListsPart() ev. im falschen
                                // Reiter

        /* Wenn man diese beiden Methoden hier aufruft, wird auch die GlassPane angezeigt.
         * Allerdings weiß ich nicht wie gut es ist, diese Dinge außerhalb des EDT zu machen.
         * Bislang waren die Aufrufe in der Methode
         * actualizeOwnListsPartInEdt()
         */
        mainTabs.createOwnListsTabFromScratch();
        showTabsWeViewedLastTime(); // Die wieder anzeigen.
        endLongTimeProcess();
//
//        SwingUtilities.invokeLater(() -> actualizeOwnListsPartInEdt());
    }

//    private void actualizeOwnListsPartInEdt() {
//        mainTabs.createOwnListsTabFromScratch();
//        showTabsWeViewedLastTime(); // Die wieder anzeigen.
//        endLongTimeProcess();
//    }

    /** Eigene Vokabellisten exportieren. */
    public void exportOwnLists() {
        mainTabs.showTabWithOwnLists();
        mainTabs.showOwnListExtraButtons();
    }

    /** Alle eigenen Vokabellisten exportieren. */
    public void exportAllOwnLists() {
        OwnLists ownLists = dataStructures.getOwnLists();
        ownLists.exportAllOwnLists();
    }

    /**
     * Zeigt auf dem Übersetzen-Buttons aller Bars das richtige Icon für die Richtung der
     * Übersetzung an.
     */
    public void showTranslationDirectionOnBarButtons() {
        mainTabs.showTranslationDirectionOnBarButtons();
    }

    /** Eine neue eigene Liste anlegen. */
    public boolean createNewOwnList() {
        OwnLists ownLists = dataStructures.getOwnLists();

        String name = ownLists.createNotExistingName("Neue Liste ");
        if (ownLists.nameCheckOk(name)) {
            return createAndEditNewOwnList(name);
        }
        else {
            throw new RuntimeException("Obwohl in OwnLists der nicht vergebene Name '" + name
                    + "' generiert wurde, besteht er die Prüfung mit nameCheckOk() nicht.");
        }
    }

    private boolean createAndEditNewOwnList(String name) {
        OwnLists ownLists = dataStructures.getOwnLists();
        OwnList ownList = ownLists.createNewList(name);

        boolean applied = editOwnList(ownList);
        if (!applied) {
            deleteOwnList(ownList);
        }
        return applied;
    }

    /** Buttons zum Exportieren, Bearbeiten und Löschen von eigene Listen ein oder ausschalten. */
    public void toggleShowOwnListButtons() {
        mainTabs.toggleShowOwnListButtons();
    }

    /** Löscht die Vokabelliste aus der Anzeige und von der Festplatte. */
    public void deleteOwnList(OwnList ownList) {
        OwnLists ownLists = dataStructures.getOwnLists();
        ownLists.deleteOwnList(ownList);
        actualizeOwnListsPart();
        ownLists.storeOwnLists();
    }

    /** Bearbeitet die Vokabelliste. */
    public boolean editOwnList(OwnList ownList) {
        OwnListPersistanceData oldData = ownList.toOwnListPersistanceData();

        OwnLists ownLists = dataStructures.getOwnLists();
        OwnListEditorDialog dialog = new OwnListEditorDialog(ownList, dataStructures, uiObjects);
        dialog.setVisible(true);
        boolean applied = dialog.isApplied();
        if (applied) {
            actualizeOwnListsPart();
            ownLists.storeOwnLists();

            OwnListPersistanceData newData = ownList.toOwnListPersistanceData();
            if (!oldData.equals(newData)) {
                adjustStoredOwnListPersistanceData(oldData, newData);
            }
        }
        return applied;
    }

    /**
     * Die Kategorie, Unterkategorie oder der Name haben sich geändert. Nun werden die drei
     * gespeicherten OwListPersistanceData überprüft und ggf. aktualisiert.
     */
    private void adjustStoredOwnListPersistanceData(OwnListPersistanceData oldData,
            OwnListPersistanceData newData) {
        Options options = dataStructures.getOptions();

        HistoricalOwnListPersistanceDataList historicalLeftOwnLists =
                options.getHistoricalLeftOwnListsInListCommander();
        HistoricalOwnListPersistanceDataList historicalRightOwnLists =
                options.getHistoricalRightOwnListsInListCommander();
        HistoricalOwnListPersistanceDataList historicalOwnLists =
                options.getHistoricalOwnLists();

        adjustHistoricalOwnLists(oldData, newData, historicalLeftOwnLists);
        adjustHistoricalOwnLists(oldData, newData, historicalRightOwnLists);
        adjustHistoricalOwnLists(oldData, newData, historicalOwnLists);
    }

    private void adjustHistoricalOwnLists(OwnListPersistanceData oldData,
            OwnListPersistanceData newData,
            HistoricalOwnListPersistanceDataList historicalOwnLists) {
        List<OwnListPersistanceData> ownListPersistenzDataList =
                historicalOwnLists.getHistoricalOwnLists();

        for (int index = 0; index < ownListPersistenzDataList.size(); ++index) {
            OwnListPersistanceData data = ownListPersistenzDataList.get(index);
            if (data.equals(oldData)) {
                ownListPersistenzDataList.set(index, newData);
            }
        }
    }

    /** Exportiert die Vokabelliste. */
    public void exportListOwnList(OwnList ownList) {
        OwnLists ownLists = dataStructures.getOwnLists();
        ownLists.exportList(ownList);
    }

    /** Zeigt den Text an, der beim Starten des Vokabeltrainers angezeigt wurde. */
    public void showSplashScreenText() {
        String title = "Startup Log";
        String text = logic.getStartupLog();
        viewTextFile(title, text);
    }

    private void viewTextFile(String title, String contents) {
        VocabularyTrainerUiTools.viewTextFile(title, contents, getLocation());
    }

    /** Zeigt die Warnungen an, die beim Starten des Vokabeltrainers aufgetreten sind. */
    public void showShowSplashWarnings() {
        List<String> startupWarnings = logic.getStartupWarnings();
        if (startupWarnings.isEmpty()) {
            GuiTools.informUser(getWindowAsComponent(), "Keine Warnungen",
                    "Beim Startup sind keine Warnungen aufgetreten.");
        }
        else {
            reallyShowWarnings(startupWarnings);
        }
    }

    private void reallyShowWarnings(List<String> startupWarnings) {
        String title = "Warnungen beim Startup";
        String text = Text.join("\n\n", startupWarnings);
        viewTextFile(title, text);
    }

    /** Zeigt die Kanji-Tabelle an. */
    public void showKanjiTable() {
        KanjiTableDialog dialog = new KanjiTableDialog(dataStructures, uiObjects,
                uiObjects.getInformer(), uiObjects.getGuiLocation());
        dialog.setVisible(true);
        setMessageLater("Die Kanji wurden als Liste angezeigt.");
    }

    /** Erstellt eine Kanji Abfrageliste von der deutschen Bedeutung aus. */
    public void createKanjiTestFromGermanMeaning() {
        String text = KanjiTestListFromGermanMeaningCreator.createKanjiTestList();

        String dialogTitle = "Kanji-Übungsliste";
        showText(text, dialogTitle);
    }

    private void showText(String text, String dialogTitle) {
        TextViewer viewer = new TextViewer(dialogTitle, getParentLocation(),
                new Dimension(1000, 900));
        viewer.useMonoscpacedText();
        viewer.setText(text);
        viewer.setVisible(true);
    }

    /** Testet die Kanji ausgehend vom Zeichen (hier im Programm). */
    public void createKanjiTestFromKanji() {
        KanjiTesterLogic tester = new KanjiTesterLogic(dataStructures, uiObjects);
        tester.test();
    }

    /** Zeigt die Hiragana an. */
    public void showHiragana() {
        String html = createHiraganaHtml();
        showHtmlInDialog("Hiragana", html);
    }

    private String createHiraganaHtml() {
        HiraganaHtmlCreator creator = new HiraganaHtmlCreator();
        creator.create();
        return creator.getHtml();
    }

    /** Zeigt die Katakana an. */
    public void showKatakana() {
        String html = createKatakanaHtml();
        showHtmlInDialog("Katakana", html);
    }

    private String createKatakanaHtml() {
        KatakanaHtmlCreator creator = new KatakanaHtmlCreator();
        creator.create();
        return creator.getHtml();
    }

    /** Zeigt die Hiragana im Browser an. */
    public void showHiraganaInBrowser() {
        String html = createHiraganaHtml();
        String filename = saveKanaTable("hiragana", html);
        SystemTools.openInStandardProgram(filename);
    }

    /** Zeigt die Katakana im Browser an. */
    public void showKatakanaInBrowser() {
        String html = createKatakanaHtml();
        String filename = saveKanaTable("katakana", html);
        SystemTools.openInStandardProgram(filename);
    }

    private String saveKanaTable(String filenameFront, String html) {
        String filename = FileHelper.concatPathes(VOCABLE_TRAINER_DIRECTORY,
                filenameFront + ".html");
        FileHelper.writeTextToFile(html, filename, Charset.UTF_8);
        return filename;
    }

    /** Importiert Gruppen von eigenen Vokabellisten von der Webseite. */
    public void importGroupsOfOwnListsFromWebsite() {
        logic.importGroupsOfOwnListsFromWebsite();
    }

    /** Zeigt alle Vokabeln mit einem bestimmten Suchbegriff an. */
    public void showAllVocablesWithASpecificSearchWord() {
        OwnLists ownLists = dataStructures.getOwnLists();
        List<String> searchWords = ownLists.getSearchWords();

        AllVocablesWithSpecificConstraintDialog dialog =
                new AllVocablesWithSpecificConstraintDialog(getProgramImage(), getLocation(),
                        "Alle Vokabeln mit einem bestimmten Suchbegriff anzeigen.", "Suchbegriff",
                        searchWords, constraint -> findVocablesWithSpecificSearchWord(constraint),
                        "Vokabeln mit dem Suchbegriff",
                        (description, vocables) -> showVocablesWithSpecialConstraintAsList(
                                description, vocables)                        );
        dialog.setVisible(true);
    }

    /**
     * Findet alle Vokabeln, die einen bestimmten Suchbegriff enthalten.
     *
     * @param searchWord
     *            Der Suchbegriff.
     * @return Die Liste mit den Vokabeln, die den Suchbegriff enthalten.
     */
    private List<Vocable> findVocablesWithSpecificSearchWord(String searchWord) {
        List<Vocable> foundVocables = new ArrayList<>();

        for (Vocable vocable : logic.collectVocablesOfAllVocabularies()) {
            List<String> searchWords = vocable.getSearchWords();
            if (searchWords.contains(searchWord)) {
                foundVocables.add(vocable);
            }
        }

        return foundVocables;
    }

    /** Zeigt alle Vokabeln mit einer bestimmten Wortart an. */
    public void showAllAllVocablesWithASpecificPartOfSpeach() {
        OwnLists ownLists = dataStructures.getOwnLists();
        List<String> partsOfSpeach = ownLists.getPartsOfSpeach();

        AllVocablesWithSpecificConstraintDialog dialog =
                new AllVocablesWithSpecificConstraintDialog(getProgramImage(), getLocation(),
                        "Alle Vokabeln mit einer bestimmten Wortart anzeigen.", "Wortart",
                        partsOfSpeach,
                        constraint -> findVocablesWithSpecificPartOfSpeach(constraint),
                        "Vokabeln mit der Wortart",
                        (description, vocables) -> showVocablesWithSpecialConstraintAsList(
                                description, vocables)
                        );
        dialog.setVisible(true);
    }

    /**
     * Findet alle Vokabeln, die eine bestimmte Wortart haben.
     *
     * @param partOfSpeach
     *            Die Wortart.
     * @return Die Liste mit den Vokabeln, die den Suchbegriff enthalten.
     */
    private List<Vocable> findVocablesWithSpecificPartOfSpeach(String partOfSpeach) {
        List<Vocable> foundVocables = new ArrayList<>();

        for (Vocable vocable : logic.collectVocablesOfAllVocabularies()) {
            List<String> partsOfSpeech = vocable.getPartsOfSpeech();
            if (partsOfSpeech.contains(partOfSpeach)) {
                foundVocables.add(vocable);
            }
        }

        return foundVocables;
    }

    /**
     * Die Vokabeln, die einer bestimmten Bedingung genügen, anzeigen.
     *
     * @param description
     *            Die Beschreibung der Menge von Vokabeln (wie sie der VocabularyListerDialog
     *            erwartet, also z.B. "neuste Vokabeln").
     * @param vocables
     *            Die Liste mit den anzuzeigenden Vokabeln.
     */
    private void showVocablesWithSpecialConstraintAsList(String description,
            List<Vocable> vocables) {
        listVocables(vocables, description);
        setMessageLater(description + " wurden als Liste angezeigt.");
    }

    /** Bearbeitet die Kanji-Mengen. */
    public void manageKanjiSets() {
        KanjiSetManagementDialog dialog = new KanjiSetManagementDialog(logic, dataStructures,
                uiObjects, getLocation());
        dialog.setVisible(true);
    }

    /** Überprüft die Integrität der Kanji-Mengen. */
    public void checkKanjiSetsIntegrity() {
        List<KanjiSet> kanjiSets = dataStructures.getKanjiSets();
        KanjiSetIntegrityChecker.checkKanjiSetsIntegrityWithSuccessInfo(kanjiSets,
                getWindowAsComponent(), getLocation());
    }

    /** Zeigt die Hiragana-Tabelle an. */
    public void showHiraganaTable() {
        HiraganaTableDialog dialog = new HiraganaTableDialog(dataStructures, uiObjects);
        dialog.setVisible(true);
        setMessageLater("Die Hiragana wurden als Liste angezeigt.");
    }

    /** Zeigt die Katakana-Tabelle an. */
    public void showKatakanaTable() {
        KatakanaTableDialog dialog = new KatakanaTableDialog(dataStructures, uiObjects);
        dialog.setVisible(true);
        setMessageLater("Die Katakana wurden als Liste angezeigt.");
    }

    /** Hiragana abfragen. */
    public void createHiraganaTest() {
        HiraganaTesterLogic tester = new HiraganaTesterLogic(dataStructures, uiObjects);
        tester.test();
    }

    /** Katakana abfragen. */
    public void createKatakanaTest() {
        KatakanaTesterLogic tester = new KatakanaTesterLogic(dataStructures, uiObjects);
        tester.test();
    }

    /** Zeigt die Verben in Wörterbuchform als Liste an. */
    public void showVerbsInWoerterbuchformAsList() {
        List<Vocable> vocables = logic.createListOfVerbsInWoerterbuchform();
        listVocables(vocables, "Verben in Wörterbuchform");
        setMessageLater("Die Verben in Wörterbuchform wurden als Liste angezeigt.");
    }

    /** Zeigt die Adjektive in Grundform (positive Gegenwart) als Liste an. */
    public void showAdjectivesInPositivePresenceAsList() {
        List<Vocable> vocables = logic.createListOfAdjectivesInPositivePresence();
        listVocables(vocables, "Adjektive in Grundform (positive Gegenwart)");
        setMessageLater("Die Adjektive in Grundform wurden als Liste angezeigt.");
    }

    /** Zeigt die い-Adjektive in Grundform (positive Gegenwart) als Liste an. */
    public void showIAdjectivesInPositivePresenceAsList() {
        List<Vocable> vocables = logic.createListOfIAdjectivesInPositivePresence();
        listVocables(vocables, "い-Adjektive in Grundform (positive Gegenwart)");
        setMessageLater("Die い-Adjektive in Grundform wurden als Liste angezeigt.");
    }

    /** Zeigt die な-Adjektive in Grundform (positive Gegenwart) als Liste an. */
    public void showNaAdjectivesInPositivePresenceAsList() {
        List<Vocable> vocables = logic.createListOfNaAdjectivesInPositivePresence();
        listVocables(vocables, "な-Adjektive in Grundform (positive Gegenwart)");
        setMessageLater("Die な-Adjektive in Grundform wurden als Liste angezeigt.");
    }

    /** Zeigt die な-Adjektive die auf い enden als Liste an. */
    public void showListOfNaAdjectivesEndingWithI() {
        List<Vocable> vocables = logic.createListOfNaAdjectivesPositivePresenceEndsWithI();
        listVocables(vocables, "な-Adjektive die in Grundform auf い enden");
        setMessageLater("Die な-Adjektive die in Grundform auf い enden wurden als Liste angezeigt.");
    }

    /** Zeigt die いる/える-Ausnahme-Verben als Liste an. */
    public void showListOfIruEruExceptionVerbs() {
        List<Vocable> vocables = logic.createListOfIruEruExceptionVerbsInWoerterbuchform();
        listVocables(vocables, "いる/える-Ausnahme-Verben in Wörterbuchform");
        setMessageLater("Die いる/える-Ausnahme-Verben in Wörterbuchform wurden als Liste angezeigt.");
    }

    /** Sucht über eine On-Lesung nach Kanji. */
    public void searchKanjiByOnLesung() {
        String onLesung = GuiTools.askUserToEnterAStringValue(getWindowAsComponent(),
                "Bitte eine ON-Lesung eingeben", ""
                        + "Bitte geben Sie die ON-Lesung ein, über die nach Kanji gesucht werden "
                        + "soll.");
        if (!onLesung.isBlank()) {
            List<Kanji> kanjiList = logic.findAllKanjiWithOnLesung(onLesung);
            String title = "Alle Kanji mit der ON-Lesung '" + onLesung + "'";
            showKanjiList(kanjiList, title);
        }
    }

    /** Sucht über eine Kun-Lesung nach Kanji. */
    public void searchKanjiByKunLesung() {
        String kunLesung = GuiTools.askUserToEnterAStringValue(getWindowAsComponent(),
                "Bitte eine kun-Lesung eingeben", ""
                        + "Bitte geben Sie die kun-Lesung ein, über die nach Kanji gesucht werden "
                        + "soll.");
        if (!kunLesung.isBlank()) {
            List<Kanji> kanjiList = logic.findAllKanjiWithKunLesung(kunLesung);
            String title = "Alle Kanji mit der kun-Lesung '" + kunLesung + "'";
            showKanjiList(kanjiList, title);
        }
    }

    /** Sucht über eine beliebige Lesung nach Kanji. */
    public void searchKanjiByAnyLesung() {
        String anyLesung = GuiTools.askUserToEnterAStringValue(getWindowAsComponent(),
                "Bitte eine beliebige Lesung eingeben", ""
                        + "Bitte geben Sie die ON- oder kun-Lesung ein, über die nach Kanji "
                        + "gesucht werden soll.");
        if (!anyLesung.isBlank()) {
            List<Kanji> kanjiList = logic.findAllKanjiWithAnyLesung(anyLesung);
            String title = "Alle Kanji mit der ON- oder kun-Lesung '" + anyLesung + "'";
            showKanjiList(kanjiList, title);
        }
    }

    /** Sucht über eine deutsche Bedeutung nach Kanji. */
    public void searchKanjiByGermanMeaning() {
        String germanMeaning = GuiTools.askUserToEnterAStringValue(getWindowAsComponent(),
                "Bitte eine deutsche Bedeutung eingeben", ""
                        + "Bitte geben Sie die deutsche Bedeutung ein, über die nach Kanji "
                        + "gesucht werden soll.");
        if (!germanMeaning.isBlank()) {
            List<Kanji> kanjiList = logic.findAllKanjiWithGermanMeaning(germanMeaning);
            String title = "Alle Kanji mit der deutschen Bedeutung '" + germanMeaning + "'";
            showKanjiList(kanjiList, title);
        }
    }

    /** Sucht über einen beliebigen Inhalt (Lesung, deutsche Bedeutung) nach Kanji. */
    public void searchKanjiByAny() {
        String any = GuiTools.askUserToEnterAStringValue(getWindowAsComponent(),
                "Bitte einem einen beliebigen Inhalt (Lesung, deutsche Bedeutung) ein", ""
                        + "Bitte geben Sie die ON- oder kun-Lesung oder die deutsche Bedeutung "
                        + "ein, über die nach Kanji gesucht werden soll.",
                        oldKanjiSearch);
        if (!any.isBlank()) {
            oldKanjiSearch = any;
            List<Kanji> kanjiList = logic.findAllKanjiWithAny(any);
            String title = "Alle Kanji mit '" + any + "'";
            showKanjiList(kanjiList, title);
        }
    }

    private void showKanjiList(List<Kanji> kanjiList, String title) {
        if (kanjiList.isEmpty()) {
            GuiTools.informUser("Keine Kanji gefunden",
                    "Es wurden keine Kanji mit den gesuchten Eigenschaften gefunden.");
        }
        else {
            KanjiTableDialog dialog = new KanjiTableDialog(kanjiList, dataStructures, uiObjects,
                    uiObjects.getInformer(), title, uiObjects.getGuiLocation());
            dialog.setVisible(true);
        }
    }

    /** Öffnet den Listen-Commander. */
    public void openListCommander() {
        OwnListCommander commander = new OwnListCommander(this, dataStructures, uiObjects);
        commander.start();

        actualizeOwnListsPart();

        OwnLists ownLists = dataStructures.getOwnLists();
        ownLists.storeOwnLists();
    }

    /**
     * Getter für die häufig verwendeten Funktionen der grafischen Oberfläche des Vokabeltrainers.
     */
    public FumikoUiObjects getUiObjects() {
        return uiObjects;
    }

    /** Erstellt Grammatikalische Tabellen von Verben oder Adjektiven. */
    public void createVerbOrAdjectiveTables() {
        GrammarTableCreator creator = new GrammarTableCreator(dataStructures, uiObjects);
        creator.create();
        if (creator.isCreationStarted()) {
            saveOptions(); // Sicherheitshalber, falls Absturz vor Beenden
        }
    }

    public void showStartupWarningsIfAny() {
        SwingUtilities.invokeLater(() -> showStartupWarningsIfAnyInEdt());
    }

    private void showStartupWarningsIfAnyInEdt() {
        List<String> startupWarnings = logic.getStartupWarnings();
        if (!startupWarnings.isEmpty()) {
            reallyShowWarnings(startupWarnings);
        }
    }

    /** Ermittelt die unbekannten Zeichen in den Kanji-Darstellungen aller Vokabeln. */
    public void determineUnknownCharactersInKanjiFieldOfVocables() {
        List<Vocabulary> vocabularies = dataStructures.getVocabularies();
        UnknownCharactersInKanjiFieldOfVocablesDeterminer determiner =
                new UnknownCharactersInKanjiFieldOfVocablesDeterminer(vocabularies);
        determiner.determine();
        List<String> unknownCharacters = determiner.getUnknownCharacters();
        String text1 = Text.joinWithBlank(unknownCharacters);
        text1 = Text.addLineBreaks(text1, 40);

        List<String> unknownCharactersWithHex = new ArrayList<>();
        for (String unknownCharacter : unknownCharacters) {
            String hex;
            try {
                hex = HexFormat.of().formatHex(unknownCharacter.getBytes("UTF-8"));
            }
            catch (UnsupportedEncodingException e) {
                //e.printStackTrace();
                hex = "nicht ermittelbar";
            }
            String unknownCharacterWithHex = unknownCharacter + "(" + hex + ")";
            unknownCharactersWithHex.add(unknownCharacterWithHex);
        }

        String text2 = Text.joinWithBlank(unknownCharactersWithHex);
        text2 = Text.addLineBreaks(text2, 40);

        String text = text1 + "\n\nMit hexadzimalen Werten:\n" + text2;

        String dialogTitle = "Unbekannte Zeichen in den Kanji-Formen der Vokabeln";
        showText(text, dialogTitle);

        boolean save = GuiTools.askUser(getWindowAsComponent(), dialogTitle,
                "Wollen sie die unbekannten Zeichen abspeichern?");
        if (save) {
            Options options = dataStructures.getOptions();
            String filename = GuiTools.saveFileAsWithTitle(
                    "Wohin wollen sie die unbekannten Zeichen abspeichern?",
                    getWindowAsComponent(), options.getLastUsedOwnListImportExportDirectory(),
                    GuiTools.createTextFileFilter());
            if (!filename.isBlank()) {
                FileHelper.writeTextToFile(text, filename, Charset.UTF_8);
            }
        }
    }

}
