/* FichothequeLib_API - Copyright (c) 2020-2022 Vincent Calame - Exemole
 * Logiciel libre donné sous triple licence :
 * 1) selon les termes de la CeCILL V2
 * 2) selon les termes de l’EUPL V.1.1
 * 3) selon les termes de la GNU GPLv3
 * Voir le fichier licences.txt
 */


package net.fichotheque.syntax;

import java.io.IOException;
import java.util.List;
import net.fichotheque.corpus.fiche.Atts;
import net.fichotheque.corpus.fiche.FicheBlock;
import net.fichotheque.corpus.fiche.H;
import net.fichotheque.corpus.fiche.P;
import net.fichotheque.corpus.fiche.S;
import net.fichotheque.corpus.fiche.TextContent;


/**
 *
 * @author Vincent Calame
 */
public final class FicheblockCodemirror {

    private FicheblockCodemirror() {

    }

    /**
     *
     * @param ficheBlock (instance de P ou H seulement)
     */
    public static void appendFicheBlock(Appendable buf, FicheBlock ficheBlock) throws IOException {
        HtmlWriter writer = new HtmlWriter(buf);
        writer.appendFicheBlock(ficheBlock);
    }

    public static void appendAttsList(Appendable buf, List<Atts> attsList) throws IOException {
        if (attsList != null) {
            HtmlWriter writer = new HtmlWriter(buf);
            writer.appendAttsList(attsList);
        }
    }

    public static void appendTextContent(Appendable buf, TextContent textContent, String supplementaryClasses) throws IOException {
        HtmlWriter writer = new HtmlWriter(buf);
        writer.supplementaryClasses = supplementaryClasses;
        writer.appendTextContent(textContent, true);
    }

    private static boolean needsQuote(String s) {
        int length = s.length();
        for (int i = 0; i < length; i++) {
            char carac = s.charAt(i);
            switch (carac) {
                case ' ':
                case '\"':
                case ')':
                case '=':
                    return true;
            }
        }
        return false;
    }

    private static void escapeString(Appendable buf, String s) throws IOException {
        int length = s.length();
        for (int i = 0; i < length; i++) {
            char carac = s.charAt(i);
            switch (carac) {
                case '\u00A0':
                case '\u202F':
                    buf.append('~');
                    break;
                default:
                    escapeHtml(buf, carac);

            }
        }
    }

    private static void escapeHtml(Appendable buf, String value) throws IOException {
        int length = value.length();
        for (int i = 0; i < length; i++) {
            char carac = value.charAt(i);
            switch (carac) {
                case '&':
                    buf.append("&amp;");
                    break;
                case '"':
                    buf.append("&quot;");
                    break;
                case '<':
                    buf.append("&lt;");
                    break;
                case '>':
                    buf.append("&gt;");
                    break;
                default:
                    buf.append(carac);
            }
        }
    }

    private static void escapeHtml(Appendable buf, char carac) throws IOException {
        switch (carac) {
            case '&':
                buf.append("&amp;");
                break;
            case '"':
                buf.append("&quot;");
                break;
            case '<':
                buf.append("&lt;");
                break;
            case '>':
                buf.append("&gt;");
                break;
            default:
                buf.append(carac);
        }
    }


    private static class HtmlWriter {

        private final Appendable buf;
        private String supplementaryClasses = "";

        private HtmlWriter(Appendable appendable) {
            this.buf = appendable;
        }

        private void appendFicheBlock(FicheBlock ficheBlock) throws IOException {
            if (ficheBlock instanceof H) {
                H h = (H) ficheBlock;
                int level = h.getLevel();
                supplementaryClasses = " cm-ficheblock-hcontent";
                appendParagraphBlockAtts(h.getAtts());
                startFirstChars();
                for (int i = 0; i < level; i++) {
                    buf.append("#");
                }
                endSpan();
                appendTextContent((H) ficheBlock, true);
            } else if (ficheBlock instanceof P) {
                P p = (P) ficheBlock;
                appendParagraphBlockAtts(p.getAtts());
                appendP(p);
            }
        }

        private void appendP(P p) throws IOException {
            boolean rajout = false;
            switch (p.getType()) {
                case P.CITATION:
                    appendFirstChar('>');
                    rajout = true;
                    break;
                case P.QUESTION:
                    appendFirstChar('?');
                    rajout = true;
                    break;
                case P.REMARK:
                    appendFirstChar('!');
                    rajout = true;
                    break;
                default:
                    break;
            }
            String source = p.getSource();
            int sourceLength = source.length();
            if (sourceLength > 0) {
                startSourceSpan();
                buf.append('@');
                for (int i = 0; i < sourceLength; i++) {
                    char carac = source.charAt(i);
                    if ((carac == '\\') || (carac == ':')) {
                        buf.append('\\');
                    }
                    escapeHtml(buf, carac);
                }
                buf.append(" :");
                rajout = true;
                endSpan();
            }
            if (rajout) {
                buf.append(' ');
            }
            appendTextContent(p, true);
        }

        private void appendAttsList(List<Atts> attsList) throws IOException {
            if (attsList.isEmpty()) {
                return;
            }
            appendOperator('[');
            for (Atts atts : attsList) {
                appendAttsWithinParenthesis(atts);
            }
            appendOperator(']');
            buf.append(' ');
        }

        private void appendParagraphBlockAtts(Atts atts) throws IOException {
            if (atts.isEmpty()) {
                return;
            }
            appendOperator('[');
            appendAttsWithinParenthesis(atts);
            appendOperator(']');
            buf.append(' ');
        }

        private boolean appendTextContent(TextContent textContent, boolean withSpecialParseChars) throws IOException {
            boolean withSupplementary = withSupplementary();
            boolean rajout = false;
            int count = textContent.size();
            for (int i = 0; i < count; i++) {
                Object obj = textContent.get(i);
                if (obj instanceof String) {
                    if (withSupplementary) {
                        startSpanWithSupplementary("");
                    }
                    escapeString(buf, (String) obj);
                    if (((String) obj).length() > 0) {
                        rajout = true;
                    }
                    if (withSupplementary) {
                        endSpan();
                    }
                } else if (obj instanceof S) {
                    rajout = true;
                    appendSpan((S) obj, withSpecialParseChars);
                }
            }
            return rajout;
        }

        private void appendAttsWithinParenthesis(Atts atts) throws IOException {
            appendOperator('(');
            appendAtts(atts, true);
            appendOperator(')');
        }

        private void appendAtts(Atts atts, boolean first) throws IOException {
            int attLength = atts.size();
            for (int i = 0; i < attLength; i++) {
                if (first) {
                    first = false;
                } else {
                    buf.append(' ');
                }
                String name = atts.getName(i);
                if (name.equals("class")) {
                    appendClassAtt(atts.getValue(i));
                } else if (name.startsWith("data-")) {
                    startAttNameSpan();
                    escapeHtml(buf, name.substring(4));
                    buf.append('=');
                    endSpan();
                    appendAttValue(atts.getValue(i));
                } else {
                    startAttNameSpan();
                    escapeHtml(buf, name);
                    buf.append('=');
                    endSpan();
                    appendAttValue(atts.getValue(i));
                }
            }
        }

        private void appendClassAtt(String value) throws IOException {
            int idx = value.indexOf('-');
            boolean done = false;
            if ((idx > 0) && (idx < (value.length() - 1))) {
                String prefix = value.substring(0, idx);
                switch (prefix) {
                    case "cm":
                    case "fbe":
                        startAttNameSpan();
                        buf.append(prefix);
                        buf.append('=');
                        endSpan();
                        appendAttValue(value.substring(idx + 1));
                        done = true;
                        break;
                }
            }
            if (!done) {
                startAttNameSpan();
                buf.append('c');
                buf.append('=');
                endSpan();
                appendAttValue(value);
            }
        }

        private void appendAttValue(String value) throws IOException {
            appendAttValue(buf, value, "cm-ficheblock-attvalue");
        }

        private void appendAttValue(Appendable buf, String value, String valueClass) throws IOException {
            if (needsQuote(value)) {
                int length = value.length();
                startStringSpan();
                buf.append('\"');
                for (int j = 0; j < length; j++) {
                    char carac = value.charAt(j);
                    switch (carac) {
                        case '\"':
                            buf.append("\\\"");
                            break;
                        case '\\':
                            buf.append("\\\\");
                            break;
                        default:
                            escapeHtml(buf, carac);
                            break;
                    }
                }
                buf.append('\"');
                endSpan();
            } else {
                startSpan(valueClass);
                escapeHtml(buf, value);
                endSpan();
            }
        }


        private void appendSpan(S span, boolean specialchars) throws IOException {
            if ((!specialchars) || (!span.getAtts().isEmpty()) || (!span.getRef().isEmpty())) {
                appendAttsSpan(span);
            } else {
                switch (span.getType()) {
                    case S.EMPHASIS:
                    case S.STRONG:
                    case S.EMSTRG:
                        appendEmphasisSpan(span);
                        break;
                    case S.BR:
                        startOperatorSpan();
                        buf.append("[%]");
                        endSpan();
                        break;
                    default:
                        appendAttsSpan(span);
                }
            }
        }

        private void appendEmphasisSpan(S span) throws IOException {
            int num = 1;
            switch (span.getType()) {
                case S.EMPHASIS:
                    num = 1;
                    break;
                case S.STRONG:
                    num = 2;
                    break;
                case S.EMSTRG:
                    num = 3;
                    break;
            }
            startOperatorSpan();
            for (int i = 0; i < num; i++) {
                buf.append('{');
            }
            endSpan();
            appendTypoSpanValue(span.getValue(), '}');
            startOperatorSpan();
            for (int i = 0; i < num; i++) {
                buf.append('}');
            }
            endSpan();
        }

        private void appendAttsSpan(S span) throws IOException {
            appendOperator('[');
            startInitialeSpan();
            buf.append(span.getTypeInitial());
            endSpan();
            Atts atts = span.getAtts();
            String ref = span.getRef();
            if ((!ref.isEmpty()) || (!atts.isEmpty())) {
                appendOperator('(');
                boolean first = true;
                if (!ref.isEmpty()) {
                    appendAttValue(buf, ref, "cm-ficheblock-ref");
                    first = false;
                }
                appendAtts(atts, first);
                appendOperator(')');
            }
            short spanType = span.getType();
            String value = span.getValue();
            if (S.isRefDefaultValue(spanType)) {
                if (value.isEmpty()) {
                    buf.append(' ');
                    value = "-";
                } else if (value.equals(ref)) {
                    value = "";
                } else {
                    buf.append(' ');
                }
            } else {
                buf.append(' ');
            }
            if (S.isTypoType(spanType)) {
                appendTypoSpanValue(value, ']');
            } else {
                appendTechSpanValue(value);
            }
            appendOperator(']');
        }

        private void appendTypoSpanValue(String value, char closingChar) throws IOException {
            boolean withSupplementary = withSupplementary();
            if (withSupplementary) {
                startSpanWithSupplementary("");
            }
            int length = value.length();
            for (int i = 0; i < length; i++) {
                char carac = value.charAt(i);
                if (carac == closingChar) {
                    buf.append('\\');
                    buf.append(closingChar);
                } else {
                    switch (carac) {
                        case '\u00A0':
                            buf.append('~');
                            break;
                        case '\u202F':
                            buf.append('~');
                            break;
                        case '\\':
                            buf.append("\\\\");
                            break;
                        default:
                            escapeHtml(buf, carac);
                            break;
                    }
                }
            }
            if (withSupplementary) {
                endSpan();
            }
        }

        private void appendTechSpanValue(String value) throws IOException {
            startSpanWithSupplementary("cm-ficheblock-span-tech");
            int length = value.length();
            for (int i = 0; i < length; i++) {
                char carac = value.charAt(i);
                switch (carac) {
                    case ']':
                        buf.append('\\');
                        buf.append(']');
                        break;
                    case '\\':
                        buf.append("\\\\");
                        break;
                    default:
                        escapeHtml(buf, carac);
                        break;
                }
            }
            endSpan();
        }

        private void appendOperator(char c) throws IOException {
            buf.append("<span class=\"cm-operator\">");
            escapeHtml(buf, c);
            buf.append("</span>");
        }

        private void appendFirstChar(char c) throws IOException {
            buf.append("<span class=\"cm-ficheblock-firstchars\">");
            escapeHtml(buf, c);
            buf.append("</span>");
        }

        private void startSourceSpan() throws IOException {
            buf.append("<span class=\"cm-ficheblock-source\">");
        }

        private void startInitialeSpan() throws IOException {
            buf.append("<span class=\"cm-ficheblock-initial\">");
        }

        private void startOperatorSpan() throws IOException {
            buf.append("<span class=\"cm-operator\">");
        }

        private void startAttNameSpan() throws IOException {
            buf.append("<span class=\"cm-ficheblock-attname\">");
        }

        private void startStringSpan() throws IOException {
            buf.append("<span class=\"cm-string\">");
        }

        private void startFirstChars() throws IOException {
            buf.append("<span class=\"cm-ficheblock-firstchars\">");
        }

        private void startSpan(String classes) throws IOException {
            buf.append("<span class=\"");
            escapeHtml(buf, classes);
            buf.append("\">");
        }

        private void startSpanWithSupplementary(String classes) throws IOException {
            buf.append("<span class=\"");
            escapeHtml(buf, classes);
            buf.append(supplementaryClasses);
            buf.append("\">");
        }

        private void endSpan() throws IOException {
            buf.append("</span>");
        }

        private boolean withSupplementary() {
            return !supplementaryClasses.isEmpty();
        }

    }

}
