package de.duehl.swing.text.html;

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

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import de.duehl.basics.collections.CollectionsHelper;
import de.duehl.basics.text.Text;
import de.duehl.basics.text.html.HtmlTool;
import de.duehl.basics.text.html.Text2HtmlBasics;
import de.duehl.basics.text.html.data.RegExpMatch;
import de.duehl.swing.text.html.characteristic.OpticalTextPartCharacteristic;
import de.duehl.swing.text.html.data.HiddenMarkedHtmlPart;

import static de.duehl.swing.ui.colors.ColorTool.getHexColorByName;
import static de.duehl.swing.ui.colors.NamedColorListFabric.*;

/**
 * Diese Klasse stellt eine abstrakte Basis für Klassen dar, welche Texte für die Anzeige in einem
 * HTML-Editor einfärben.
 *
 * @version 1.01     2023-05-23
 * @author Christian Dühl
 */

public abstract class Text2HtmlBase {

    private static final String PLACEHOLDER_START = "[[###-";
    private static final String PLACEHOLDER_END = "-###]]";

    private static final String WRONG_BACKGROUND_COLOR = LIGHTRED;
    private static final String HIGHLIGHT_BACKGROUND_COLOR = LIGHTYELLOW;

    public static final int STANDARD_HTML_FONT_SIZE = 20;

    /** Der Text der hier bearbeitet wird. */
    private String text;

    /** Der originale Text wie er war, bevor er hier bearbeitet wurde. */
    private String originalText;

    /** HTML-Schriftgröße. */
    private int htmlFontSize;

    /** Gibt an, ob ein monospaced Font benutzt wird. */
    private boolean useMonospacedFont;

    /**
     * Gibt an, ob Treffer im Text auf das ganze Wort ausgeweitet werden. Dann wird z.B. auch
     * 'Apfelbaum' rot und nicht nur der Wortteil 'Apfel', wenn man nach 'Apfel' sucht.
     */
    private boolean expandHighlightedToWholeWord;

    /** Gibt an, ob im Text < und > in die HTML-Varianten umgesetzt wurde. */
    private boolean encodeHtmlAmpLtAndGt;

    /**
     * Gibt an, ob HTML-Sonderzeichen (bis auf das Leerzeichen) im Text in die HTML-Varianten
     * umgesetzt wurden.
     */
    private boolean encodeHtmlWithoutSpaces;

    /** Gibt an, ob Änderungen am Text mit Anfang und Ende markiert werden. */
    private boolean markChanges;

    /**
     * Ausschnitt aus der Markierung von Anfang und Ende eine Änderung, wenn diese vorgenommen
     * werden.
     */
    private String changesMarkSemanticPart;

    /** Gibt an, ob Hervorhebungen innerhalb von anderen Hervorhebungen zugelassen werden. */
    private boolean allowNestedSpans;

    /**
     * Gibt an, ob die versteckten Teile automatisch hier gesammelt und auch automatisch vor
     * finalizeHtmlText() wieder eingesetzt werden
     */
    private boolean collectAndReinsertHiddenParts;

    /** Sammlung aller versteckten Ersetzungen. */
    private final List<HiddenMarkedHtmlPart> allCollectedHiddenParts;

    /**
     * Gibt an, ob Suchbegriffe auch dann gefunden werden, wenn ein Zeilenumbruch in ihnen
     * vorkommt.
     *
     * Das ist sinnvoll und sollte nur abgeschaltet werden, wenn es zu viel Performance frisst.
     */
    private boolean searchTextsAroundLineBreaks;

    /** Konstruktor mit einer initialen Schriftgröße des HTML-Textes von 20. */
    protected Text2HtmlBase() {
        this(STANDARD_HTML_FONT_SIZE);
    }

    /**
     * Konstruktor.
     *
     * @param htmlFontSize
     *            Initiale Schriftgröße des HTML-Textes.
     */
    protected Text2HtmlBase(int htmlFontSize) {
        this.htmlFontSize = htmlFontSize;
        expandHighlightedToWholeWord = false;
        markChanges = false;
        changesMarkSemanticPart = "";
        allowNestedSpans = false;
        collectAndReinsertHiddenParts = false;
        allCollectedHiddenParts = new ArrayList<>();
        searchTextsAroundLineBreaks = false;
    }

    /** Setter für den Text der hier bearbeitet wird. */
    public final void setText(String text) {
        this.text = text;
        originalText = text;
    }

    /** Setzt den Text, aber nicht den originalen Text. */
    protected final void setOnlyText(String text) {
        this.text = text;
    }

    /** Getter für den Text der hier bearbeitet wird. */
    public final String getText() {
        return text;
    }

    /**  Getter für den originalen Text wie er war, bevor er hier bearbeitet wurde. */
    public final String getOriginalText() {
        return originalText;
    }

    /** Gibt an, ob der übergebene Teil im Text enthalten ist. */
    protected final boolean textContains(String part) {
        return text.contains(part);
    }

    /**
     * Gibt den Index des übergebenen Teils im Text an. Ist er nicht enthalten, wird -1 zurück
     * gegeben.
     */
    protected final int textIndexOf(String part) {
        return text.indexOf(part);
    }

    /**
     * Gibt den Index des übergebenen Teils im Text an, bei dem dieser ab dem übergebenen Index
     * gefunden wird. Ist er nicht enthalten, wird -1 zurück gegeben.
     */
    protected final int textIndexOf(String part, int index) {
        return text.indexOf(part, index);
    }

    /**
     * Legt fest, dass Treffer im Text auf das ganze Wort ausgeweitet werden. Dann wird z.B. auch
     * 'Apfelbaum' rot und nicht nur der Wortteil 'Apfel', wenn man nach 'Apfel' sucht.
     */
    protected final void expandHighlightedToWholeWord() {
        expandHighlightedToWholeWord = true;
    }

    /**
     * Legt fest, dass die versteckten Teile automatisch hier gesammelt und auch automatisch vor
     * finalizeHtmlText() wieder eingesetzt werden
     */
    protected final void collectAndReinsertHiddenParts() {
        collectAndReinsertHiddenParts = true;
    }

    /** Setzt < und > im Text in die HTML-Varianten um. */
    protected final void handleAmpLtAndGt() {
        text = HtmlTool.encodeHtmlAmpLtAndGt(text);
        encodeHtmlAmpLtAndGt = true;
    }

    /** Setzt HTML-Sonderzeichen (bis auf das Leerzeichen) im Text in die HTML-Varianten um. */
    protected final void encodeHtmlWithoutSpaces() {
        text = encodeHtmlWithoutSpaces(text);
        encodeHtmlWithoutSpaces = true;
    }

    /**
     * Ersetzt bestimmte UTF-8 Zeichen durch HTML-Codes in einem Text. Das Leerzeichen " " wird
     * hierbei aber nicht in "&nbsp;" umgesetzt.
     *
     * Anders als in HtmlTool.encodeHtmlWithoutSpaces() werden hier die Ersetzung von einfachen
     * Anführungszeichen in &apos; rückgängig gemacht, da das in der Swingkomponente nicht
     * interpretiert wird. Da steht dann wirklich &apos; im Text.
     *
     * @param text
     *            Zu bearbeitender HTML-Text.
     * @return Text mit entfernten HTML-Zeichen.
     */
    public final static String encodeHtmlWithoutSpaces(String input) {
        String output =  HtmlTool.encodeHtmlWithoutSpaces(input);
        /*
         * Da die Swing-Componente &apos; nicht richtig darstellt:
         */
        output = output.replace("&apos;", "'");
        return output;
    }

    /** Setzt die HTML-Schriftgröße. */
    public final void setHtmlFontSize(int htmlFontSize) {
        this.htmlFontSize = htmlFontSize;
    }

    /** Legt fest, einen monospaced Font zu nutzen. */
    public final void useMonospacedFont() {
        useMonospacedFont = true;
    }

    /** Gibt an, ob Hervorhebungen innerhalb von anderen Hervorhebungen zugelassen werden. */
    public final void allowNestedSpans() {
        allowNestedSpans = true;
    }

    /**
     * Legt fest, dass Suchbegriffe auch dann gefunden werden, wenn ein Zeilenumbruch in ihnen
     * vorkommt.
     *
     * Das ist sinnvoll und sollte nur abgeschaltet werden, wenn es zu viel Performance frisst.
     */
    public final void searchTextsAroundLineBreaks() {
        searchTextsAroundLineBreaks = true;
    }


    /* ---- ab hier Methoden zum Färben des Textes: ---- */

    /**
     * Färbt den übergebenen String im Text fett.
     *
     * Leere Werte werden nicht eingefärbt.
     *
     * @param searchString
     *            Einzufärbender Suchbegriff.
     * @param htmlColor
     *            Zu vergebende Farbe für HTML im Format "ff0000".
     */
    @Deprecated
    protected final void colorBold(String searchString, String htmlColor) {
        OpticalTextPartCharacteristic characteristics = new OpticalTextPartCharacteristic();
        characteristics.setBold(true);
        characteristics.setForegroundHexColor(htmlColor);
        replaceInTextWithCharacteristics(searchString, characteristics);
    }

    /**
     * Färbt einen bestimmten Suchbegriff im Text fett und in Warn-Hintergrundfarbe ein.
     *
     * Leere Werte werden nicht eingefärbt.
     *
     * @param searchString
     *            Einzufärbender Suchbegriff.
     * @param htmlColor
     *            Zu vergebende Farbe für HTML im Format "ff0000".
     */
    @Deprecated
    protected final void colorBoldAndWrong(String searchString, String htmlColor) {
        colorBoldWithBackground(searchString, htmlColor, getHexColorByName(WRONG_BACKGROUND_COLOR));
    }

    @Deprecated
    private void colorBoldWithBackground(String searchString, String foregroundColor,
            String backgroundColor) {
        OpticalTextPartCharacteristic characteristics = new OpticalTextPartCharacteristic();
        characteristics.setBold(true);
        characteristics.setForegroundHexColor(foregroundColor);
        characteristics.setBackgroundHexColor(backgroundColor);
        characteristics.setUseBackgroundColor(true);
        replaceInTextWithCharacteristics(searchString, characteristics);
    }

    /**
     * Färbt einen bestimmten Suchbegriff im Text fett und in hervorhebender Hintergrundfarbe ein.
     *
     * Leere Werte werden nicht eingefärbt.
     *
     * @param searchString
     *            Einzufärbender Suchbegriff.
     * @param htmlColor
     *            Zu vergebende Farbe für HTML im Format "ff0000".
     */
    @Deprecated
    protected final void colorBoldAndHighlighted(String searchString, String htmlColor) {
        colorBoldWithBackground(searchString, htmlColor,
                getHexColorByName(HIGHLIGHT_BACKGROUND_COLOR));
    }

    /**
     * Färbt einen bestimmten Suchbegriff im Text ein und macht ihn kursiv.
     *
     * Leere Werte werden nicht eingefärbt.
     *
     * @param searchString
     *            Einzufärbender Suchbegriff.
     * @param htmlColor
     *            Zu vergebende Farbe für HTML im Format "ff0000".
     */
    @Deprecated
    protected final void colorItalic(String searchString, String htmlColor) {
        OpticalTextPartCharacteristic characteristics = new OpticalTextPartCharacteristic();
        characteristics.setItalic(true);
        characteristics.setForegroundHexColor(htmlColor);
        replaceInTextWithCharacteristics(searchString, characteristics);
    }

    /**
     * Ersetzt im HTML-Text alle übergebene Textabschnitte (und alle ähnlichen Varianten)
     * entsprechend den hier festgelegten Eigenschaften. Jedes Vorkommen wird hierbei ersetzt.
     *
     * @param searchStrings
     *            Liste mit zu verändernden Textabschnitten.
     * @param characteristics
     *            Die Eigenschaften der zu verändernden Textabschnitten, welche in einem
     *            HTML-Editor eingefärbt dargestellt wird.
     */
    protected final void replaceInTextWithCharacteristics(List<String> searchStrings,
            OpticalTextPartCharacteristic characteristics) {
        for (String searchString : searchStrings) {
            replaceInTextWithCharacteristics(searchString, characteristics);
        }
    }

    /**
     * Ersetzt im HTML-Text den übergebenen Textabschnitt (und alle ähnlichen Varianten)
     * entsprechend den hier festgelegten Eigenschaften. Jedes Vorkommen wird hierbei ersetzt.
     *
     * @param searchString
     *            Zu verändernder Textabschnitt.
     * @param characteristics
     *            Die Eigenschaften des zu verändernden Textabschnitts, welcher in einem
     *            HTML-Editor eingefärbt dargestellt wird.
     */
    protected final void replaceInTextWithCharacteristics(String searchString,
            OpticalTextPartCharacteristic characteristics) {
        String openingSpan = characteristics.createOpeningSpan();
        colorWithHandlingOfFuzziness(searchString, openingSpan);
    }

    private void colorWithHandlingOfFuzziness(String searchString, String openingSpan) {
        for (String fuzzySearchString : createFuzzyListOfSearchStrings(searchString)) {
            color(fuzzySearchString, openingSpan);
        }
    }

    /**
     * Erzeugt eine Liste mit ähnlichen Suchbegriffen, die dann alle hervorgehoben werden.
     *
     * Für echtes Verhalten überschrieben! Diese Methode erzeugt eine Liste mit einem Element.
     */
    protected List<String> createFuzzyListOfSearchStrings(String searchString) {
        return CollectionsHelper.buildListFrom(searchString);
    }

    private void color(String searchString, String openingSpan) {
        if (!searchString.isBlank()) {
            colorNotBlankSearchString(searchString, openingSpan);
        }
    }

    private void colorNotBlankSearchString(String searchString, String openingSpan) {
        if (expandHighlightedToWholeWord && Text.containsOnlyWordChars(searchString)) {
            List<String> highlightWords = Text.getDistinctWordsContaining(originalText,
                    searchString);
            // 'originalText', denn im 'text' könnte in den Platzhaltern was gefunden werden.
            CollectionsHelper.sortStringListByLengthDescanding(highlightWords);
            highlightWords = postProccessHighlightWords(highlightWords);
            for (String highLightWord : highlightWords) {
                //System.out.println("Highlight: " + highLightWord);
                reallyColorNotBlankSearchString(highLightWord, openingSpan);
            }
        }
        else if (searchTextsAroundLineBreaks) {
            reallyColorNotBlankSearchStringWithLineBreaks(searchString, openingSpan);
        }
        else {
            reallyColorNotBlankSearchString(searchString, openingSpan);
        }
    }

    private List<String> postProccessHighlightWords(List<String> highlightWords) {
        List<String> postProccessedHighlightWords = new ArrayList<>();

        for (String highlightWord : highlightWords) {
            String postProccessedHighlightWord = postProccessHighlightWord(highlightWord);
            postProccessedHighlightWords.add(postProccessedHighlightWord);
        }

        return postProccessedHighlightWords;
    }

    private String postProccessHighlightWord(String highlightWord) {
        if (encodeHtmlAmpLtAndGt) {
            return HtmlTool.encodeHtmlAmpLtAndGt(highlightWord);
        }
        else if (encodeHtmlWithoutSpaces) {
            return encodeHtmlWithoutSpaces(highlightWord);
        }
        else {
            return highlightWord;
        }
    }

    private void reallyColorNotBlankSearchStringWithLineBreaks(String searchString,
            String openingSpan) {
        List<RegExpMatch> regExpMatches = determineRegExpMatches(searchString);
        makeStartsAndEndsNotOverlapping(regExpMatches);
        sortByStartDescanding(regExpMatches);
        replaceTextForAllRegExpMatches(regExpMatches, openingSpan);
    }

    private List<RegExpMatch> determineRegExpMatches(String searchString) {
        List<RegExpMatch> regExpMatches = new ArrayList<>();

        String searchRegex = createSearchRegex(searchString);
        Pattern pattern = Pattern.compile(searchRegex);

        Matcher matcher = pattern.matcher(text);
        while (matcher.find()) {
            int start = matcher.start();
            int end = matcher.end();
            String before = text.substring(0, start);
            String after = text.substring(end);
            if (canWeColor(before, after)) {
                RegExpMatch regExpMatch = new RegExpMatch(start, end, matcher.group());
                regExpMatches.add(regExpMatch);
            }
        }

        return regExpMatches;
    }

    private String createSearchRegex(String searchString) {
        List<String> nonBlankParts = Text.splitByWhitespace(searchString); // Tabulator und Space
        StringBuilder builder = new StringBuilder();
        boolean first = true;
        for (String nonBlankPart : nonBlankParts) {
            if (first) {
                first = false;
            }
            else {
                builder.append("(?:\\s|<br/>)+");
            }
            builder.append(Pattern.quote(nonBlankPart));
        }

        return builder.toString();
    }

    private void makeStartsAndEndsNotOverlapping(List<RegExpMatch> regExpMatches) {
        List<Integer> indicesToDelete = new ArrayList<>();

        for (int index1 = 0; index1 < regExpMatches.size() - 1; ++index1) {
            RegExpMatch regExpMatch1 = regExpMatches.get(index1);
            for (int index2 = index1 + 1; index2 < regExpMatches.size(); ++index2) {
                RegExpMatch regExpMatch2 = regExpMatches.get(index2);
                if (overlap(regExpMatch1, regExpMatch2)) {
                    indicesToDelete.add(index2);
                }
            }
        }

        Collections.reverse(indicesToDelete);

        for (int indexToDelete : indicesToDelete) {
            regExpMatches.remove(indexToDelete);
        }
    }

    private boolean overlap(RegExpMatch regExpMatch1, RegExpMatch regExpMatch2) {
        int start1 = regExpMatch1.getStart();
        int end1 = regExpMatch1.getEnd();
        int start2 = regExpMatch2.getStart();
        int end2 = regExpMatch2.getEnd();

        if (start1 >= start2 && start1 <= end2) {
            return true;
        }
        else if (start2 >= start1 && start2 <= end1) {
            return true;
        }
        else if (start1 <= start2 && end1 >= end2) {
            return true;
        }
        else if (start2 <= start1 && end2 >= end1) {
            return true;
        }
        else {
            return false;
        }
    }

    private void sortByStartDescanding(List<RegExpMatch> regExpMatches) {
        Collections.sort(regExpMatches, new Comparator<RegExpMatch>() {
            @Override
            public int compare(RegExpMatch o1, RegExpMatch o2) {
                return o2.getStart() - o1.getStart();
            }
        });
    }

    private void replaceTextForAllRegExpMatches(List<RegExpMatch> regExpMatches, String openingSpan) {
        for (RegExpMatch regExpMatch : regExpMatches) {
            replaceTextForRegExpMatch(regExpMatch, openingSpan);
        }
    }

    private void replaceTextForRegExpMatch(RegExpMatch regExpMatch, String openingSpan) {
        String before = text.substring(0, regExpMatch.getStart());
        String after = text.substring(regExpMatch.getEnd());
        String replacement = getStartMark()
                + openingSpan + regExpMatch.getMatchedPart() + "</span>"
                + getEndMark();
        text = before + replacement + after;

    }

//    private void reallyColorNotBlankSearchStringWithLineBreaksX(String searchString,
//            String openingSpan) {
//        String searchRegex = createSearchRegex(searchString);
//        Pattern pattern = Pattern.compile(searchRegex);
//        Matcher matcher = pattern.matcher(text);
//        List<Integer> starts = new ArrayList<>();
//        List<Integer> ends = new ArrayList<>();
//        while (matcher.find()) {
//            String before = text.substring(0, matcher.start());
//            String after = text.substring(matcher.end());
//            if (canWeColor(before, after)) {
//                String replacement = getStartMark()
//                        + openingSpan + matcher.group() + "</span>"
//                        + getEndMark();
//                text = before + replacement + after;
//                matcher = pattern.matcher(text); // sonst wird auf dem nicht ersetzten Text weiter
//                                                 // operiert!
//            }
//        }
//    }

    private void reallyColorNotBlankSearchString(String searchString, String openingSpan) {
        List<Integer> indices = Text.findAllPositionsWithoutOverlapping(searchString, text);
        CollectionsHelper.sortDescanding(indices);
        for (int index : indices) {
            String before = text.substring(0, index);
            String after = text.substring(index + searchString.length());
            if (canWeColor(before, after)) {
                String replacement = getStartMark()
                        + openingSpan + searchString + "</span>"
                        + getEndMark();
                text = before + replacement + after;
            }
        }
    }

    private boolean canWeColor(String before, String after) {
        if (Text.endsWithRegex(before, "color:.{0,2}")) {
            return false;
        }

        int lastPlaceholerStart = before.lastIndexOf(PLACEHOLDER_START);
        int lastPlaceholerEnd = before.lastIndexOf(PLACEHOLDER_END);
        if (lastPlaceholerStart > -1 && lastPlaceholerStart > lastPlaceholerEnd) {
            return false;
        }

        int firstPlaceholerStart = after.indexOf(PLACEHOLDER_START);
        int firstPlaceholerEnd = after.indexOf(PLACEHOLDER_END);
        if (firstPlaceholerEnd > -1 && firstPlaceholerEnd < firstPlaceholerStart) {
            return false;
        }

        return true;
    }

    @SuppressWarnings("unused")
    private void reallyColorNotBlankSearchStringOld(String searchString, String openingSpan) {
        String regex = ""
                + "(?s)"
                + "(?<!color:.{0,2})"
                + "(" + Pattern.quote(searchString) + ")";
        String replacement = getStartMark()
                + openingSpan + "$1" + "</span>"
                + getEndMark();
        text = text.replaceAll(regex, replacement);

        /*
         * Erklärung zu (?s):
         *
         * (?s) entspricht DOTALL und sagt Java, dass . auch Zeilenumbrüche matcht.
         *
         *
         * Erklärung zu (?<!color:.{0,2}):
         *
         * Es gab den Fall, dass die Postleitzahl 6600 zu Ersetzungen in der davor eingefärbten
         * Straße führte, die da lautete:
         *     <span style="color:ff6600;font-weight:bold">ul. Zhaltusha 23</span>
         * das führte dann zu
         *     <span style="color:ff
         *         <span style="color:2e8b57;font-weight:bold">6600</span>
         *     ;font-weight:bold">ul. Zhaltusha 23</span>
         * und dementsprechend Anzeigefehlern.
         */
    }

    /**
     * Ersetzt im HTML-Text die übergebene Nummer in beliebigen mit Leerzeichen aufgefüllten
     * Varianten entsprechend den hier festgelegten Eigenschaften. Jedes Vorkommen wird hierbei
     * ersetzt.
     *
     * @param number
     *            Hervorzuhebende Nummer.
     * @param characteristics
     *            Die Eigenschaften der zu verändernden Nummer, welche in einem HTML-Editor
     *            eingefärbt dargestellt wird.
     */
    protected final void replaceNumberWithDifferentSpacesInTextWithCharacteristics(String number,
            OpticalTextPartCharacteristic characteristics) {
        String openingSpan = characteristics.createOpeningSpan();
        for (String numberVariation :
                Text.searchNumberWithDifferentSpacesInText(number, getText())) {
            color(numberVariation, openingSpan);
        }
    }

    /**
     * Schließt die Bearbeitung des HTML-Textes ab und umgibt ihn mit der richtigen Schriftgröße
     * und den passenden HTML-Befehlen für den Body.
     */
    protected final void finalizeHtmlText() {
        if (collectAndReinsertHiddenParts) {
            reinsertHiddenMarkedParts(allCollectedHiddenParts);
        }

        if (!allowNestedSpans) {
            text = Text2HtmlBasics.removeNestedSpans(text);
        }
        text = Text2HtmlBasics.surroundTextWithFontSize(text, htmlFontSize);
        if (useMonospacedFont) {
            text = Text2HtmlBasics.surroundTextWithHtmlWithMonospacedFont(text);
        }
        else {
            text = Text2HtmlBasics.surroundTextWithHtml(text);
        }
    }

    /**
     * Ersetzt im HTML-Text alle übergebene Textabschnitte (und alle ähnlichen Varianten)
     * entsprechend den hier festgelegten Eigenschaften. Jedes Vorkommen wird hierbei ersetzt.
     *
     * Im Anschluss werden die Ersetzungen im Text durch Platzhalter ersetzt, um sie vor späteren
     * Ersetzungen im Text zu schützen. Die Liste mit den Ersetzungen wird übergeben und muss
     * später vor dem Aufruf von finalizeHtmlText() mit Hilfe der Methode
     * reinsertHiddenMarkedParts() wieder eingesetzt werden.
     *
     * Hierbei wird für jeden einzelnen Suchbegriff die Ersetzung am Ende versteckt, damit nicht
     * spätere Suchbegriffe aus der Liste innerhalb von vorher ersetzten Einträgen Dinge ersetzen.
     * Deshalb werden sowohl der changesMarkSemanticPart als auch der hiddenSemanticPart um
     * laufende Nummern am Ende ergänzt, damit diese alle unterschiedlich sind.
     *
     * @param searchStrings
     *            Liste mit zu verändernden Textabschnitten.
     * @param characteristics
     *            Die Eigenschaften der zu verändernden Textabschnitten, welche in einem
     *            HTML-Editor eingefärbt dargestellt wird.
     * @param characteristics
     *            Die Eigenschaften des zu verändernden Textabschnitts, welcher in einem
     *            HTML-Editor eingefärbt dargestellt wird.
     * @param changesMarkSemanticPart
     *            benannter Teil der Kommentare zum Markieren von Start und Ende der Veränderung im
     *            Text.
     * @param hiddenSemanticPart
     *            Ausschnitt aus dem Platzhalter der markierten Bereiche.
     */
    protected final List<HiddenMarkedHtmlPart> replaceInTextWithCharacteristicsAndHide(
            List<String> searchStrings, OpticalTextPartCharacteristic characteristics,
            String changesMarkSemanticPart, String hiddenSemanticPart) {
        List<HiddenMarkedHtmlPart> allHiddenParts = new ArrayList<>();
        int count = 0;
        for (String searchString : searchStrings) {
            ++count;
            startMarkChanges(changesMarkSemanticPart + "-" + count);

            replaceInTextWithCharacteristics(searchString, characteristics);

            List<HiddenMarkedHtmlPart> hiddenParts =
                    hideMarkedParts(hiddenSemanticPart + "-" + count);
            allHiddenParts.addAll(hiddenParts);
            endMarkChanges();
        }
        allCollectedHiddenParts.addAll(allHiddenParts);
        return allHiddenParts;
    }

    /**
     * Ersetzt im HTML-Text den übergebenen Textabschnitt (und alle ähnlichen Varianten)
     * entsprechend den hier festgelegten Eigenschaften. Jedes Vorkommen wird hierbei ersetzt.
     *
     * Im Anschluss werden die Ersetzungen im Text durch Platzhalter ersetzt, um sie vor späteren
     * Ersetzungen im Text zu schützen. Die Liste mit den Ersetzungen wird übergeben und muss
     * später vor dem Aufruf von finalizeHtmlText() mit Hilfe der Methode
     * reinsertHiddenMarkedParts() wieder eingesetzt werden.
     *
     * @param searchString
     *            Zu verändernder Textabschnitt.
     * @param characteristics
     *            Die Eigenschaften des zu verändernden Textabschnitts, welcher in einem
     *            HTML-Editor eingefärbt dargestellt wird.
     * @param changesMarkSemanticPart
     *            benannter Teil der Kommentare zum Markieren von Start und Ende der Veränderung im
     *            Text.
     * @param hiddenSemanticPart
     *            Ausschnitt aus dem Platzhalter der markierten Bereiche.
     */
    protected final List<HiddenMarkedHtmlPart> replaceInTextWithCharacteristicsAndHide(
            String searchString, OpticalTextPartCharacteristic characteristics,
            String changesMarkSemanticPart, String hiddenSemanticPart) {
        startMarkChanges(changesMarkSemanticPart);

        replaceInTextWithCharacteristics(searchString, characteristics);

        List<HiddenMarkedHtmlPart> hiddenParts = hideMarkedParts(hiddenSemanticPart);
        endMarkChanges();
        allCollectedHiddenParts.addAll(hiddenParts);
        return hiddenParts;
    }

    /**
     * Beginnt mit der Markierung von Änderungen.
     *
     * Verwendet man diese Methode in einer Text2Html-Klasse, so sollte man prüfen, ob man nicht
     * besser eine der replaceInTextWithCharacteristicsAndHide()-Methoden aufruft. Sinn macht es.
     * wenn das Ersetzen innerhalb gefundener Suchstrings optional ist.
     *
     * @param changesMarkSemanticPart
     *            benannter Teil der Kommentare zum Markieren von Start und Ende der Veränderung im
     *            Text.
     */
    protected final void startMarkChanges(String changesMarkSemanticPart) {
        markChanges = true;
        this.changesMarkSemanticPart = changesMarkSemanticPart;
    }

    /**
     * Beendet das Markieren von Änderungen.
     *
     * Verwendet man diese Methode in einer Text2Html-Klasse, so sollte man prüfen, ob man nicht
     * besser eine der replaceInTextWithCharacteristicsAndHide()-Methoden aufruft. Sinn macht es.
     * wenn das Ersetzen innerhalb gefundener Suchstrings optional ist.
     */
    protected final void endMarkChanges() {
        markChanges = false;
        changesMarkSemanticPart = "";
    }

    /**
     * Gibt die Startmarkierung für Veränderungen an.
     *
     * Werden diese nicht markiert, wird der leere String zurückgegeben.
     */
    private final String getStartMark() {
        if (markChanges) {
            return "<!-- " + changesMarkSemanticPart + "-START -->";
        }
        else {
            return "";
        }
    }

    /**
     * Gibt die Endmarkierung für Veränderungen an.
     *
     * Werden diese nicht markiert, wird der leere String zurückgegeben.
     */
    private final String getEndMark() {
        if (markChanges) {
            return "<!-- " + changesMarkSemanticPart + "-END -->";
        }
        else {
            return "";
        }
    }

    /**
     * Ersetzt alle markierten veränderten Bereiche durch Platzhalter.
     *
     * Verwendet man diese Methode in einer Text2Html-Klasse, so sollte man prüfen, ob man nicht
     * besser eine der replaceInTextWithCharacteristicsAndHide()-Methoden aufruft. Sinn macht es.
     * wenn das Ersetzen innerhalb gefundener Suchstrings optional ist.
     *
     * @param hiddenSemanticPart
     *            Ausschnitt aus dem Platzhalter der markierten Bereiche.
     * @return Liste mit den Ersetzungen.
     */
    protected final List<HiddenMarkedHtmlPart> hideMarkedParts(String hiddenSemanticPart) {
        List<HiddenMarkedHtmlPart> hiddenMarkedHtmlParts = new ArrayList<>();

        boolean replacing = true;
        int replaceNumber = 0;
        while (replacing) {
            int startMarkIndex = text.indexOf(getStartMark());
            if (startMarkIndex == -1) {
                replacing = false;
            }
            else {
                int endMarkIndex = text.indexOf(getEndMark(), startMarkIndex + 1);
                if (endMarkIndex == -1) {
                    replacing = false;
                }
                else {
                    int markedTextStartIndex = startMarkIndex + getStartMark().length();
                    int markedTextEndIndex = endMarkIndex;
                    int start = startMarkIndex;
                    int end = endMarkIndex + getEndMark().length();
                    String cutOut = text.substring(markedTextStartIndex, markedTextEndIndex);
                    String placeHolder = PLACEHOLDER_START + hiddenSemanticPart + "-"
                            + ++replaceNumber + PLACEHOLDER_END;
                    text = Text.replaceInText(text, placeHolder, start, end);
                    HiddenMarkedHtmlPart part = new HiddenMarkedHtmlPart(cutOut, placeHolder);
                    hiddenMarkedHtmlParts.add(part);
                }
            }
        }

        return hiddenMarkedHtmlParts;
    }

    /**
     * Fügt vorher versteckte veränderte Bereiche wieder in den Text ein.
     *
     * @param hiddenMarkedHtmlParts
     *            Liste mit den Ersetzungen.
     */
    protected final void reinsertHiddenMarkedParts(
            List<HiddenMarkedHtmlPart> hiddenMarkedHtmlParts) {
        for (HiddenMarkedHtmlPart part : hiddenMarkedHtmlParts) {
            String cutOut = part.getCutOut();
            String placeHolder = part.getPlaceHolder();
            text = text.replace(placeHolder, cutOut);
        }
    }

}
