package de.duehl.swing.ui.text.autocompletion;

/*
 * Copyright 2021 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import java.awt.event.ActionEvent;
import java.util.Collections;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;

import de.duehl.basics.collections.CollectionsHelper;
import de.duehl.basics.text.Text;

/**
 * Diese Klasse stellt das Tool zum Bearbeiten der "NoGoodTags" 1 oder 2 dar.
 *
 * @version 1.01     2021-07-27
 * @author Christian Dühl
 */

public class AutoCompletionTextComponentExtender {

    private static final int MINIMAL_LENGTH_FOR_AUTOCOMPLETION_START = 2;

    private static final String COMMIT_ACTION = "commit";

    /** Die Text-Komponente, der das Auto-Vervollständigen hinzugefügt werden soll. */
    private final JTextComponent textComponent;

    /** Liste der Begriffe, die vervollständigt werden. */
    private final List<String> autoCompletionWords;

    /** Der Modus in dem sich die Textkomponente befindet: Normal oder in Autovervollständigung. */
    private AutoCompletionMode autoCompletionMode;

    /** Gibt an, mit welcher Taste die Autovervollständigung aktiviert werden soll. */
    private AutoCompletionKey autoCompletionKey;

    /** Gibt an, ob hinter der Autovervollständigung ein Leerzeichen eingefügt werden soll. */
    private boolean addSpaceBehindAutoCompletion;

    /** Position vor der Änderung des Textes. Gibt man einen Buchstaben ein, dann die davor! */
    private int changePosition;

    /** Text aus der Text-Komponente bis zum Änderungs-Position. */
    private String textContentBeforeChangePosition;

    /** Index des Anfangs des Wortes, in dem geändert wird. */
    private int indexOfWordStart;

    /** Minimale Größe ab der die Autovervollständigung aktiviert ist. Default ist 2. */
    private int minimalLengthForAutoCompletionStart;

    /**
     * Konstruktor.
     *
     * @param textComponent
     *            Die Text-Komponente, der das Auto-Vervollständigen hinzugefügt werden soll.
     * @param autoCompletionWords
     *            Liste der Begriffe, die vervollständigt werden.
     */
    public AutoCompletionTextComponentExtender(JTextComponent textComponent,
            List<String> autoCompletionWords) {
        this.textComponent = textComponent;
        this.autoCompletionWords = CollectionsHelper.toLowerCase(autoCompletionWords);

        /*
         * Die Liste der Worte zu denen vervollständigt werden soll, muss auf natürliche Weise
         * aufsteigend Sortiert werden, damit die binäre Suche in
         * internalInsertWithAutoCompletion() funktioniert. Daher sortieren wir:
         */
        Collections.sort(autoCompletionWords);
        CollectionsHelper.toLowerCase(autoCompletionWords);

        autoCompletionKey = AutoCompletionKey.ENTER;
        addSpaceBehindAutoCompletion = true;
        minimalLengthForAutoCompletionStart = MINIMAL_LENGTH_FOR_AUTOCOMPLETION_START;
    }

    /** Schaltet von der Aktivierung mit Return auf Aktivierung mit der Leertaste um. */
    public void changeActivationKeyToSpace() {
        autoCompletionKey = AutoCompletionKey.SPACE;
    }

    /** Legt fest, dass hinter der Autovervollständigung kein Leerzeichen eingefügt werden soll. */
    public void doNotInsertSpaceBehindCompletion() {
        addSpaceBehindAutoCompletion = false;
    }

    /** Setzt die minimale Größe ab der die Autovervollständigung aktiviert ist. Default ist 2. */
    public void setMinimalLengthForAutoCompletionStart(int minimalLengthForAutoCompletionStart) {
        if (minimalLengthForAutoCompletionStart < 1
                || minimalLengthForAutoCompletionStart > 80) {
            throw new IllegalArgumentException("Der Wert muss zwischen 1 und 80 liegen!");
        }
        this.minimalLengthForAutoCompletionStart = minimalLengthForAutoCompletionStart;
    }

    /**
     * Erweitert die übergebene Komponente um eine Autovervollständigung mit den hinterlegten
     * Begriffen.
     *
     */
    public void extendAutoCompletion() {
        autoCompletionMode = AutoCompletionMode.INSERT;
        initComponents();
    }

    private void initComponents() {
        textComponent.getDocument().addDocumentListener(createDocumentListener());

        InputMap inputMap = textComponent.getInputMap();
        inputMap.put(KeyStroke.getKeyStroke(autoCompletionKey.getKeyStrokeString()), COMMIT_ACTION);

        ActionMap actionMap = textComponent.getActionMap();
        actionMap.put(COMMIT_ACTION, new CommitAction());
    }

    /**
     * Inserts the specified text at the specified position. Does nothing if the model is null or
     * if the text is null or empty.
     *
     * Nur die JTextArea hat insert, aber nicht JTextComponent und auch nicht JTextField, daher
     * habe ich die insert-Methode aus JTextArea hier nachgebildet.
     *
     * @param str
     *            the text to insert
     * @param pos
     *            the position at which to insert &gt;= 0
     * @exception IllegalArgumentException
     *                if pos is an invalid position in the model
     */
    private void insert(String str, int pos) {
        Document doc = textComponent.getDocument();
        if (doc != null) {
            try {
                doc.insertString(pos, str, null);
            }
            catch (BadLocationException e) {
                throw new IllegalArgumentException(e.getMessage());
            }
        }
    }

    private DocumentListener createDocumentListener() {
        return new DocumentListener() {
            @Override
            public void changedUpdate(DocumentEvent event) {
            }

            @Override
            public void removeUpdate(DocumentEvent event) {
            }

            @Override
            public void insertUpdate(DocumentEvent event) {
                if (event.getLength() != 1) {
                    return;
                }
                else {
                    insertWithAutoCompletion(event.getOffset());
                }
            }
        };
    }

    private void insertWithAutoCompletion(int position) {
        changePosition = position;
        textContentBeforeChangePosition = getContentBeforePosition();

        indexOfWordStart = findWordStart();
        int wordLengthUntilPosition = (changePosition + 1) - indexOfWordStart;

        if (wordLengthUntilPosition >= minimalLengthForAutoCompletionStart) {
            searchForAutoCompletion();
        }
    }

    /** Liefert den Teil des Textes bis zu Position an der die Änderung beginnt zurück. */
    private String getContentBeforePosition() {
        try {
            return textComponent.getText(0, changePosition + 1);
        }
        catch (BadLocationException exception) {
            throw new RuntimeException(
                    "Fehler beim Einfügen in Text-Komponente mit Autovervollständigung: "
                            + "\n\t" + "changePosition = " + changePosition
                            + "\n\t" + "text           = " + textComponent.getText()
                            + "\n\t" + "Fehler         = " + exception.getLocalizedMessage()
                            ,
                    exception);
        }
    }

    /** Sucht den Beginn des Wortes an der übergebenen Position. */
    private int findWordStart() {
        for (int wordStart = changePosition; wordStart >= 0; --wordStart) {
            char charAtWordStart = textContentBeforeChangePosition.charAt(wordStart);
            if (!Character.isLetter(charAtWordStart)) {
                return wordStart + 1;
            }
        }

        return 0;
    }

    /**
     * Sucht nach dem passenden zu vervollständigenden Wort und ergänzt dessen Rest bei Erfolg.
     *
     * Die binäre Suche liefert bei nicht gefundenen Worten den Index, an dem dieses Wort eingefügt
     * werden müsste,
     *     (-(insertion point) - 1)
     * zurück.
     *
     * Daher ist
     *     autoCompletionWords.get(-index - 1)
     * das zu überprüfende Wort. Wenn es mit dem Anfang des eingegebenen Wortes beginnt,
     * vervollständigen wir es.
     *
     * Anderenfalls geht die Komponente in den normalen Einfügemodus über.
     */
    private void searchForAutoCompletion() {
        String prefix = textContentBeforeChangePosition.substring(indexOfWordStart);
        prefix = Text.toLowerCase(prefix);
        int index = Collections.binarySearch(autoCompletionWords, prefix);

        /*
         * Wenn man einmal Dinge mit Umlauten vervollständigen will, muss man die binarySearch
         * durch was eigenes ersetzen.
         */

        if (index < 0 && -index <= autoCompletionWords.size()) {
            String match = autoCompletionWords.get(-index - 1);
            if (match.startsWith(prefix)) {
                String completion = match.substring(changePosition + 1 - indexOfWordStart);

                // We cannot modify Document from within notification,
                // so we submit a task that does the change later
                SwingUtilities.invokeLater(() -> completeAutoCompletion(completion));
            }
        }
        else {
            autoCompletionMode = AutoCompletionMode.INSERT;
        }
    }

    private void completeAutoCompletion(String completion) {
        int position = changePosition + 1;
        insert(completion, position);
        textComponent.setCaretPosition(position + completion.length());
        textComponent.moveCaretPosition(position);
        autoCompletionMode = AutoCompletionMode.COMPLETION;
    }

    private class CommitAction extends AbstractAction {
        private static final long serialVersionUID = 1L;

        @Override
        public void actionPerformed(ActionEvent event) {
            commitAutoCompleteAction();
        }
    }

    private void commitAutoCompleteAction() {
        if (autoCompletionMode == AutoCompletionMode.COMPLETION) {
            int position = textComponent.getSelectionEnd();
            if (addSpaceBehindAutoCompletion) {
                insert(" ", position);
                textComponent.setCaretPosition(position + 1);
            }
            else {
                textComponent.setCaretPosition(position);
            }
            autoCompletionMode = AutoCompletionMode.INSERT;
        }
        else {
            //textComponent.replaceSelection(
            //        autoCompletionKey.getTextInsertionNotInAutocompleteMode());
            // Abgeschaltet 27.07.2021
        }
    }

}
