package de.duehl.swing.ui.highlightingeditor.syntax;

/*
 * Copyright 2017 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.Color;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;
import javax.swing.text.StyledDocument;

import de.duehl.basics.collections.CollectionsHelper;
import de.duehl.basics.debug.Assure;
import de.duehl.basics.text.Text;
import de.duehl.swing.ui.highlightingeditor.syntax.data.TokenWithPosition;
import de.duehl.swing.ui.highlightingeditor.syntax.data.TokenWithStyleAndPosition;

/**
 * Diese Klasse stellt das Syntax-Hervorhebung des bearbeitbaren Editors mit Syntax-Highlighting
 * dar.
 *
 * @version 1.01     2017-12-05
 * @author Christian Dühl
 */

public class SyntaxHighlighting {

    /* Namen der Stile: */
    private static final String DEFAULT_STYLE = "default";
    private static final String KEYWORDS_STYLE = "keywords";
    private static final String SYMBOLS_STYLE = "symbols";
    private static final String OTHER_STYLE = "other";

    /* Farben für den regulären Stil: */
    private static final Color TEXT_COLOR = Color.BLACK;

    /** Mit diesem Zeichen wird gefundener Text vor weiteren Suchen überdeckt. */
    private static final char HIDE_CHAR = '#';

    /** Für die Schlüsselworte ('public', 'class', ...) verwendete Art der Syntax-Hervorhebung. */
    private final HighlightingTokens keywords;

    /** Für die Symbole ('{', '[', '=', ...) verwendete Art der Syntax-Hervorhebung. */
    private final HighlightingTokens symbols;

    /** Sonstige Hervorhebungen. */
    private final List<HighlightingTokens> other;

    /** Der mit Syntax-Hervorhebung zu versehende Text in highlightText(). */
    private String text;

    /** Das StyledDocument des Editors. */
    private StyledDocument styledDocument;

    /** Gibt an, ob Treffer auch Wortteile auszeichnen. */
    private boolean markInWordParts;

    /**
     * Konstruktor.
     *
     * @param keywordsHighlightingType
     *            Art der Syntax-Hervorhebung für die Schlüsselworte ('public', 'class', ...).
     * @param symbolsHighlightingType
     *            Art der Syntax-Hervorhebung für die Symbole ('{', '[', '=', ...).
     */
    public SyntaxHighlighting(HighlightingType keywordsHighlightingType,
            HighlightingType symbolsHighlightingType) {
        keywords = new HighlightingTokens(keywordsHighlightingType);
        symbols = new HighlightingTokens(symbolsHighlightingType);
        other = new ArrayList<>();
    }

    public void addKeyword(String keyword) {
        keywords.addToken(keyword);
    }

    public void addSymbol(String symbol) {
        symbols.addToken(symbol);
    }

    /** Fügt eine weitere Liste von Tokens zur Syntax-Hervorhebung hinzu. */
    public void addOtherHighlightingTokens(HighlightingTokens tokens) {
        other.add(tokens);
    }

    /**
     * Initiiert die zu verwendenden Stile des StyledDocument des Editors. Diese Methode sollte nur
     * einmal aufgerufen werden.
     *
     * @param styledDocument
     *            StyledDocument des Editors.
     */
    public void initStyles(StyledDocument styledDocument) {
        try {
            tryToInitStyles(styledDocument);
        }
        catch (Exception ecxeption) {
            throw new RuntimeException("Es trat ein Fehler beim Initialisieren der Stile auf.",
                    ecxeption);
        }
    }

    private void tryToInitStyles(StyledDocument styledDocument) {
        this.styledDocument = styledDocument;

        Style regularStyle = createDefaultStyle();

        createKeywordsStyle(regularStyle);
        createSymolsStyle(regularStyle);
        createOtherStyles(regularStyle);
    }

    private Style createDefaultStyle() {
        Style defaultStyle = StyleContext.getDefaultStyleContext().
                getStyle(StyleContext.DEFAULT_STYLE);

        Style regularStyle = styledDocument.addStyle(DEFAULT_STYLE, defaultStyle);
        StyleConstants.setForeground(regularStyle, TEXT_COLOR);

        return regularStyle;
    }

    private void createKeywordsStyle(Style parentStyle) {
        createOneStyle(parentStyle, KEYWORDS_STYLE, keywords.getHighlightingType());
    }

    private void createSymolsStyle(Style parentStyle) {
        createOneStyle(parentStyle, SYMBOLS_STYLE, symbols.getHighlightingType());
    }

    private void createOtherStyles(Style parentStyle) {
        int i = 0;
        for (HighlightingTokens tokens : other) {
            HighlightingType highlightingType = tokens.getHighlightingType();
            String name = createOtherStyleName(++i);
            createOneStyle(parentStyle, name, highlightingType);
        }
    }

    private String createOtherStyleName(int i) {
        return OTHER_STYLE + "_" + i;
    }

    private void createOneStyle(Style parentStyle, String name, HighlightingType highlightingType) {
        Style symbolsStyle = styledDocument.addStyle(name, parentStyle);

        Color color = highlightingType.getColor();
        StyleConstants.setForeground(symbolsStyle, color);

        boolean bold = highlightingType.isBold();
        StyleConstants.setBold(symbolsStyle, bold);
    }

    /**
     * Initiiert die zu verwendenden Stile des StyledDocument des Editors. Diese Methode sollte nur
     * einmal aufgerufen werden.
     *
     * Vorher Caret-Position merken und hinterher wieder setzen, falls zulässig!
     *
     * @param text
     *            Der einzufärbende Text.
     * @param styledDocument
     *            StyledDocument des Editors.
     */
    public void highlightText(String text, StyledDocument styledDocument) {
        try {
            tryToHighlightText(text, styledDocument);
        }
        catch (Exception ecxeption) {
            throw new RuntimeException("Es trat ein Fehler beim Initialisieren der Stile auf.",
                    ecxeption);
        }
    }

    private void tryToHighlightText(String text, StyledDocument styledDocument) {
        this.text = text;
        this.styledDocument = styledDocument;

        clearDocument();

        List<TokenWithStyleAndPosition> textParts = generateTextParts();
        Collections.sort(textParts); // sortiert nach Textpositionen

        for (TokenWithStyleAndPosition textWithStyle : textParts) {
            String textPart = textWithStyle.getText();
            String styleName = textWithStyle.getStyle();
            addTextPart(textPart, styleName);
        }
    }

    private void clearDocument() {
        try {
            styledDocument.remove(0, styledDocument.getLength());
        }
        catch (Exception exception) {
            throw new RuntimeException(exception);
        }
    }

    private List<TokenWithStyleAndPosition> generateTextParts() {
        List<TokenWithStyleAndPosition> textParts = new ArrayList<>();

        textParts.addAll(generateKeywordsTextParts());
        textParts.addAll(generateSymbolsTextParts());
        textParts.addAll(generateOtherTextParts());
        textParts.addAll(generateRegularTextParts());

        return textParts;
    }

    private List<TokenWithStyleAndPosition> generateKeywordsTextParts() {
        return generateTextParts(keywords, KEYWORDS_STYLE);
    }

    private List<TokenWithStyleAndPosition> generateSymbolsTextParts() {
        return generateTextParts(symbols, SYMBOLS_STYLE);
    }

    private List<TokenWithStyleAndPosition> generateOtherTextParts() {
        List<TokenWithStyleAndPosition> otherTextParts = new ArrayList<>();

        int i = 0;
        for (HighlightingTokens otherTokens : other) {
            String name = createOtherStyleName(++i);
            otherTextParts.addAll(generateTextParts(otherTokens, name));
        }

        return otherTextParts;
    }

    private List<TokenWithStyleAndPosition> generateRegularTextParts() {
        List<TokenWithStyleAndPosition> foundTokens = new ArrayList<>();

        int startIndex = 0;
        boolean searchOn = true;
        while (searchOn) {
            int position = text.indexOf(HIDE_CHAR, startIndex);
            if (position == -1) {
                searchOn = false;
                String token = text.substring(startIndex);
                if (!token.isEmpty()) {
                    TokenWithStyleAndPosition foundToken = new TokenWithStyleAndPosition(token,
                            DEFAULT_STYLE, startIndex);
                    foundTokens.add(foundToken);
                }
            }
            else {
                String token = text.substring(startIndex, position);
                TokenWithStyleAndPosition foundToken = new TokenWithStyleAndPosition(token,
                        DEFAULT_STYLE, startIndex);
                foundTokens.add(foundToken);
                String hideChar = Character.toString(HIDE_CHAR);
                while (position + 1 < text.length()
                        && text.substring(position + 1, position + 2).equals(hideChar)) {
                    ++position;
                }
                startIndex = position + 1;
            }
        }

        return foundTokens;
    }

    private List<TokenWithStyleAndPosition> generateTextParts(HighlightingTokens tokens,
            String style) {
        List<String> symbolTokens = getSortedTokens(tokens);
        markInWordParts = tokens.isMarkInWordParts();
        List<TokenWithPosition> foundTokens = findTokensInText(symbolTokens);
        return createFoundTokenListWithStyle(foundTokens, style);
    }

    private List<String> getSortedTokens(HighlightingTokens tokens) {
        List<String> sortedTokens = new ArrayList<>();
        sortedTokens.addAll(tokens.getTokens());
        CollectionsHelper.sortStringListByLengthDescanding(sortedTokens);
        return sortedTokens;
    }

    private List<TokenWithPosition> findTokensInText(List<String> tokens) {
        List<TokenWithPosition> foundTokens = new ArrayList<>();

        for (String token : tokens) {
            foundTokens.addAll(findOneTokenInText(token));
        }

        return foundTokens;

    }

    private List<TokenWithPosition> findOneTokenInText(String token) {
        List<TokenWithPosition> foundTokens = new ArrayList<>();

        int startIndex = 0;
        boolean searchOn = true;
        while (searchOn) {
            int position = text.indexOf(token, startIndex);
            if (position == -1) {
                searchOn = false;
            }
            else {
                if (markInWordParts || checkPossibleFoundToken(token, position)) {
                    TokenWithPosition foundToken = new TokenWithPosition(token, position);
                    foundTokens.add(foundToken);
                    hideOneFoundTokenFromText(foundToken);
                }
                startIndex = position + token.length();
            }
        }

        return foundTokens;
    }

    /**
     * Für ein richtiges Syntax-Highlighting muss man zwischen dem Schlüsselwort 'String' und der
     * eigenen Klasse "StringVergleich" unterscheiden und darf nicht deren ersten Teil mit als
     * Schlüsselwort färben.
     *
     * @param token
     *            Mögliches gefundenes Token
     * @param position
     *            Position des Tokens im Text.
     * @return Gibt an, ob es wirklich ein brauchbares Fundstück ist, oder nur Teil von etwas
     *         längerem.
     */
    private boolean checkPossibleFoundToken(String token, int position) {
        return checkPossibleFoundTokenFront(position)
                && checkPossibleFoundTokenRear(position + token.length());
    }

    private boolean checkPossibleFoundTokenFront(int startPostion) {
        if (startPostion == 0) {
            return true;
        }

        String charBeforeToken = text.substring(startPostion - 1, startPostion);
        return charIsValidForTokenSeparation(charBeforeToken);
    }

    private boolean checkPossibleFoundTokenRear(int endPosition) {
        if (endPosition == text.length()) {
            return true;
        }

        String charAfterToken = text.substring(endPosition, endPosition + 1);
        return charIsValidForTokenSeparation(charAfterToken);
    }

    private boolean charIsValidForTokenSeparation(String charNextToToken) {
        return !charNextToToken.matches("[0-9a-zäöüßA-ZÄÖÜ_]");
    }

    private List<TokenWithStyleAndPosition> createFoundTokenListWithStyle(
            List<TokenWithPosition> foundTokens, String tokenStyle) {
        List<TokenWithStyleAndPosition> tokensWithStyle = new ArrayList<>();

        for (TokenWithPosition token : foundTokens) {
            tokensWithStyle.add(new TokenWithStyleAndPosition(token, tokenStyle));
        }

        return tokensWithStyle;
    }

    private void hideOneFoundTokenFromText(TokenWithPosition token) {
        int oldFullLength = text.length();

        String tokenText = token.getText();
        int start = token.getTextPosition();
        int length = tokenText.length();
        int end = start + length;
        text = Text.replaceInTextWithEqualLengthCharacterSequence(text, HIDE_CHAR, start, end);

        int newFullLength = text.length();
        Assure.isEqual(oldFullLength, newFullLength);
    }

    private void addTextPart(String textPart, String styleName) {
        try {
            Style style = styledDocument.getStyle(styleName);
            Assure.notNull("Der Stil '" + styleName + "' wurde nicht gefunden! "
                    + "Das zugehörige Objekt aus dem Dokument", style);
            String cleanedTextPart = textPart.replaceAll("\r?\n", "\n");
            styledDocument.insertString(styledDocument.getLength(), cleanedTextPart, style);
        }
        catch (Exception exception) {
            throw new RuntimeException(exception);
        }
    }

}
