/* FichothequeLib_API - Copyright (c) 2006-2025 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.SubsetKey;
import net.fichotheque.corpus.fiche.Atts;
import net.fichotheque.corpus.fiche.Cdatadiv;
import net.fichotheque.corpus.fiche.Code;
import net.fichotheque.corpus.fiche.Div;
import net.fichotheque.corpus.fiche.FicheBlock;
import net.fichotheque.corpus.fiche.H;
import net.fichotheque.corpus.fiche.Insert;
import net.fichotheque.corpus.fiche.Li;
import net.fichotheque.corpus.fiche.Ln;
import net.fichotheque.corpus.fiche.P;
import net.fichotheque.corpus.fiche.ParagraphBlock;
import net.fichotheque.corpus.fiche.S;
import net.fichotheque.corpus.fiche.Table;
import net.fichotheque.corpus.fiche.Td;
import net.fichotheque.corpus.fiche.TextContent;
import net.fichotheque.corpus.fiche.Tr;
import net.fichotheque.corpus.fiche.Ul;
import net.fichotheque.corpus.fiche.ZoneBlock;
import net.mapeadores.util.localisation.Lang;


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

    private FicheblockSyntax() {
    }

    public static Parameters parameters() {
        return new Parameters();
    }

    public static String toString(List<FicheBlock> ficheBlockList, Parameters parameters) {
        if (ficheBlockList.isEmpty()) {
            return "";
        }
        StringBuilder stringBuilder = new StringBuilder(1024);
        FicheblockSyntaxHandler handler = new FicheblockSyntaxHandler(stringBuilder);
        try {
            appendFicheBlocks(handler, ficheBlockList, parameters);
        } catch (IOException ioe) {
        }
        return stringBuilder.toString();
    }

    public static void appendFicheBlocks(FicheblockSyntaxHandler handler, List<FicheBlock> ficheBlockList, Parameters parameters) throws IOException {
        appendFicheBlocks(handler, ficheBlockList, parameters, "");
    }

    private static void appendFicheBlocks(FicheblockSyntaxHandler handler, List<FicheBlock> ficheBlockList, Parameters parameters, String positionPrefix) throws IOException {
        boolean hPrevious = false;
        int position = 0;
        for (FicheBlock ficheBlock : ficheBlockList) {
            position++;
            boolean isH = false;
            if (position > 1) {
                handler.append('\n');
            }
            if (ficheBlock instanceof H) {
                isH = true;
                H h = (H) ficheBlock;
                int level = h.getLevel();
                if ((!hPrevious) && (position > 1)) {
                    handler.append('\n');
                }
                handler.storeLine(positionPrefix, position);
                appendParagraphBlockAtts(handler, h);
                for (int i = 0; i < level; i++) {
                    handler.append("#");
                }
                appendTextContent(handler, (H) ficheBlock, parameters);
                handler.append('\n');
            } else if (ficheBlock instanceof Ul) {
                startUl(handler, (Ul) ficheBlock, parameters, positionPrefix, position);
            } else if (ficheBlock instanceof Code) {
                handler.storeLine(positionPrefix, position);
                appendCode(handler, (Code) ficheBlock, parameters);
            } else if (ficheBlock instanceof Table) {
                appendTable(handler, (Table) ficheBlock, parameters, positionPrefix, position);
            } else if (ficheBlock instanceof Insert) {
                handler.storeLine(positionPrefix, position);
                appendInsert(handler, (Insert) ficheBlock, parameters);
            } else if (ficheBlock instanceof Div) {
                appendDiv(handler, (Div) ficheBlock, parameters, positionPrefix, position);
            } else if (ficheBlock instanceof Cdatadiv) {
                handler.storeLine(positionPrefix, position);
                appendCdatadiv(handler, (Cdatadiv) ficheBlock, parameters);
            } else {
                P p = (P) ficheBlock;
                handler.storeLine(positionPrefix, position);
                appendParagraphBlockAtts(handler, p);
                boolean done = appendP(handler, p, parameters);
                if (done) {
                    handler.append('\n');
                }
            }
            hPrevious = isH;
        }
    }

    public static boolean appendTextContent(Appendable buf, TextContent textContent, Parameters parameters) throws IOException {
        return appendTextContent(buf, textContent, parameters.withSpecialParseChars);
    }

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

    private static void appendCode(Appendable buf, Code code, Parameters parameters) throws IOException {
        short type = code.getType();
        appendZoneBlockAtts(buf, code);
        buf.append("+++");
        if (type != Code.TYPE_NONE) {
            buf.append(Code.typeToInitiale(type));
        }
        buf.append('\n');
        appendZoneBlockElements(buf, code, parameters);
        String indentString = "  ";
        for (Ln ln : code) {
            appendParagraphBlockAtts(buf, ln);
            String s = ln.getValue();
            int indent = ln.getIndentation();
            for (int j = 0; j < indent; j++) {
                buf.append(indentString);
            }
            if (s.length() > 0) {
                char carac = s.charAt(0);
                switch (carac) {
                    case '+':
                    case '\\':
                        buf.append('\\');
                        break;
                    case '[':
                        if (s.length() > 1) {
                            if (s.charAt(1) == '(') {
                                buf.append('\\');
                            }
                        }
                        break;
                }
            }
            buf.append(s);
            buf.append('\n');
        }
        buf.append("+++++++++++");
        buf.append('\n');
    }

    private static void appendDiv(FicheblockSyntaxHandler handler, Div div, Parameters parameters, String positionPrefix, int position) throws IOException {
        appendZoneBlockAtts(handler, div);
        handler.append("!!!");
        Lang lang = div.getLang();
        if (lang != null) {
            handler.append(lang.toString());
        }
        handler.append('\n');
        appendZoneBlockElements(handler, div, parameters);
        appendFicheBlocks(handler, div, parameters, positionPrefix + position + ".");
        handler.append("!!!!!!!!!!!");
        handler.append('\n');
    }

    private static void appendCdatadiv(Appendable buf, Cdatadiv cdatadiv, Parameters parameters) throws IOException {
        appendZoneBlockAtts(buf, cdatadiv);
        buf.append("???");
        buf.append('\n');
        appendZoneBlockElements(buf, cdatadiv, parameters);
        buf.append(cdatadiv.getCdata());
        buf.append('\n');
        buf.append("???????????");
        buf.append('\n');
    }

    private static void appendInsert(Appendable buf, Insert insert, Parameters parameters) throws IOException {
        short type = insert.getType();
        appendZoneBlockAtts(buf, insert);
        buf.append(":::");
        buf.append(Insert.typeToInitiale(type));
        if (type == Insert.LETTRINE_TYPE) {
            buf.append(Insert.positionToInitiale(insert.getPosition()));
        }
        buf.append('\n');
        appendZoneBlockTextContent(buf, 'n', insert.getNumero(), parameters);
        appendZoneBlockTextContent(buf, 'l', insert.getLegende(), parameters);
        SubsetKey subsetKey = insert.getSubsetKey();
        if (subsetKey != null) {
            if (subsetKey.isAddendaSubset()) {
                buf.append("d= ");
            } else {
                buf.append("i= ");
            }
            buf.append(subsetKey.getSubsetName());
            buf.append("/");
            buf.append(String.valueOf(insert.getId()));
            buf.append('\n');
        } else {
            buf.append("s= ");
            buf.append(insert.getSrc());
            buf.append('\n');
            appendInt(buf, 'w', insert.getWidth());
            appendInt(buf, 'h', insert.getHeight());
        }
        appendString(buf, 'r', insert.getRef());
        appendZoneBlockTextContent(buf, 'a', insert.getAlt(), parameters);
        appendZoneBlockTextContent(buf, 'c', insert.getCredit(), parameters);
        buf.append(":::::::::::");
        buf.append('\n');
    }

    private static void appendString(Appendable buf, char carac, String value) throws IOException {
        if (value.length() == 0) {
            return;
        }
        buf.append(carac);
        buf.append("= ");
        buf.append(value);
        buf.append('\n');
    }

    private static void appendInt(Appendable buf, char carac, int value) throws IOException {
        if (value < 0) {
            return;
        }
        buf.append(carac);
        buf.append("= ");
        buf.append(String.valueOf(value));
        buf.append('\n');
    }

    private static void appendZoneBlockElements(Appendable buf, ZoneBlock zoneBlock, Parameters parameters) throws IOException {
        appendZoneBlockTextContent(buf, 'n', zoneBlock.getNumero(), parameters);
        appendZoneBlockTextContent(buf, 'l', zoneBlock.getLegende(), parameters);
        buf.append('\n');
    }

    private static void appendZoneBlockTextContent(Appendable buf, char carac, TextContent textContent, Parameters parameters) throws IOException {
        if (textContent.isEmpty()) {
            return;
        }
        buf.append(carac);
        buf.append("= ");
        FicheblockSyntax.appendTextContent(buf, textContent, parameters);
        buf.append('\n');
    }

    private static void appendTable(FicheblockSyntaxHandler handler, Table table, Parameters parameters, String positionPrefix, int position) throws IOException {
        String childPositionPrefix = positionPrefix + position + ".";
        appendZoneBlockAtts(handler, table);
        handler.append("===");
        handler.append('\n');
        appendZoneBlockElements(handler, table, parameters);
        int tableSize = table.size();
        for (int i = 0; i < tableSize; i++) {
            Tr tr = table.get(i);
            if (tr.isEmpty()) {
                continue;
            }
            handler.storeLine(childPositionPrefix, i + 1);
            Atts atts = tr.getAtts();
            if (!atts.isEmpty()) {
                handler.append('[');
                appendAttsWithinParenthesis(handler, atts);
                handler.append(']');
                handler.append('\n');
            }
            for (Td td : tr) {
                appendTd(handler, td, parameters);

            }
            handler.append('\n');
        }
        handler.append("===========");
        handler.append('\n');
    }

    private static void appendTd(Appendable buf, Td td, Parameters parameters) throws IOException {
        appendParagraphBlockAtts(buf, td);
        if (td.getType() == Td.HEADER) {
            buf.append('#');
        }
        boolean rajout = FicheblockSyntax.appendTextContent(buf, td, parameters);
        if (!rajout) {
            buf.append('-');
        }
        buf.append('\n');
    }

    private static void appendZoneBlockAtts(Appendable buf, ZoneBlock zoneBlock) throws IOException {
        Atts atts = zoneBlock.getAtts();
        if (atts.isEmpty()) {
            return;
        }
        buf.append('[');
        appendAttsWithinParenthesis(buf, zoneBlock.getAtts());
        buf.append(']');
        buf.append('\n');
    }

    private static void appendParagraphBlockAtts(Appendable buf, ParagraphBlock paragraphBlock) throws IOException {
        Atts atts = paragraphBlock.getAtts();
        if (atts.isEmpty()) {
            return;
        }
        buf.append('[');
        appendAttsWithinParenthesis(buf, paragraphBlock.getAtts());
        buf.append(']');
        buf.append(' ');
    }

    private static void appendAttsWithinParenthesis(Appendable buf, Atts atts) throws IOException {
        buf.append('(');
        appendAtts(buf, atts, true);
        buf.append(')');
    }


    private static void appendAttValue(Appendable buf, String value) throws IOException {
        if (needsQuote(value)) {
            int length = value.length();
            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:
                        buf.append(carac);
                        break;
                }
            }
            buf.append('\"');
        } else {
            buf.append(value);
        }
    }

    private static void startUl(FicheblockSyntaxHandler handler, Ul ul, Parameters parameters, String positionPrefix, int position) throws IOException {
        appendUl(handler, ul, 1, parameters, positionPrefix + position + ".");
    }

    private static void appendUl(FicheblockSyntaxHandler handler, Ul ul, int level, Parameters parameters, String positionPrefix) throws IOException {
        Atts atts = ul.getAtts();
        if (!atts.isEmpty()) {
            handler.append('[');
            appendAttsWithinParenthesis(handler, atts);
            handler.append(']');
            handler.append('\n');
        }
        int position = 0;
        for (Li li : ul) {
            position++;
            appendLi(handler, li, level, parameters, positionPrefix, position);
        }
    }

    private static void appendLi(FicheblockSyntaxHandler handler, Li li, int currentLevel, Parameters parameters, String positionPrefix, int position) throws IOException {
        String childPositionPrefix = positionPrefix + position + ".";
        P firstP = (P) li.get(0);
        handler.storeLine(childPositionPrefix, 1);
        Atts liAtts = li.getAtts();
        int liAttLength = liAtts.size();
        Atts pAtts = firstP.getAtts();
        int pAttLength = pAtts.size();
        if ((liAttLength != 0) || (pAttLength != 0)) {
            handler.append('[');
            appendAttsWithinParenthesis(handler, liAtts);
            if (pAttLength > 0) {
                appendAttsWithinParenthesis(handler, pAtts);
            }
            handler.append(']');
            handler.append(' ');
        }
        for (int i = 0; i < currentLevel; i++) {
            handler.append('-');
        }
        handler.append(' ');
        appendP(handler, firstP, parameters);
        handler.append('\n');
        int ficheBlockCount = li.size();
        for (int i = 1; i < ficheBlockCount; i++) {
            FicheBlock ficheBlock = li.get(i);
            int childPosition = i + 1;
            if (ficheBlock instanceof P) {
                handler.storeLine(childPositionPrefix, childPosition);
                Atts followingPAtts = ficheBlock.getAtts();
                if (!followingPAtts.isEmpty()) {
                    handler.append('[');
                    appendAttsWithinParenthesis(handler, followingPAtts);
                    handler.append(']');
                }
                for (int j = 0; j < currentLevel; j++) {
                    handler.append('_');
                }
                handler.append(' ');
                appendP(handler, (P) ficheBlock, parameters);
                handler.append('\n');
            } else if (ficheBlock instanceof Ul) {
                Ul ul = (Ul) ficheBlock;
                appendUl(handler, ul, currentLevel + 1, parameters, childPositionPrefix + childPosition + ".");
            }
        }
    }

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

    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 appendSpan(Appendable buf, S span, boolean specialchars) throws IOException {
        if ((!specialchars) || (!span.getAtts().isEmpty()) || (!span.getRef().isEmpty())) {
            appendAttsSpan(buf, span);
        } else {
            switch (span.getType()) {
                case S.EMPHASIS:
                case S.STRONG:
                case S.EMSTRG:
                    appendEmphasisSpan(buf, span);
                    break;
                case S.BR:
                    buf.append("[%]");
                    break;
                default:
                    appendAttsSpan(buf, span);
            }
        }
    }

    private static void appendEmphasisSpan(Appendable buf, 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;
        }
        for (int i = 0; i < num; i++) {
            buf.append('{');
        }
        appendTypoSpanValue(buf, span.getValue(), '}');
        for (int i = 0; i < num; i++) {
            buf.append('}');
        }
    }

    private static void appendAttsSpan(Appendable buf, S span) throws IOException {
        buf.append('[');
        buf.append(span.getTypeInitial());
        Atts atts = span.getAtts();
        String ref = span.getRef();
        if ((!ref.isEmpty()) || (!atts.isEmpty())) {
            buf.append("(");
            boolean first = true;
            if (!ref.isEmpty()) {
                appendAttValue(buf, ref);
                first = false;
            }
            appendAtts(buf, atts, first);
            buf.append(")");
        }
        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(buf, value, ']');
        } else {
            appendTechSpanValue(buf, value);
        }
        buf.append(']');
    }

    private static void appendTypoSpanValue(Appendable buf, String value, char closingChar) throws IOException {
        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:
                        buf.append(carac);
                        break;
                }
            }
        }
    }

    private static void appendTechSpanValue(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('\\');
                    buf.append(']');
                    break;
                case '\\':
                    buf.append("\\\\");
                    break;
                default:
                    buf.append(carac);
                    break;
            }
        }
    }

    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:
                    buf.append(carac);

            }
        }
    }

    private static void appendAtts(Appendable buf, 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(buf, atts.getValue(i));
            } else if (name.startsWith("data-")) {
                buf.append(name.substring(4));
                buf.append('=');
                appendAttValue(buf, atts.getValue(i));
            } else {
                buf.append(name);
                buf.append('=');
                appendAttValue(buf, atts.getValue(i));
            }
        }
    }


    private static void appendClassAtt(Appendable buf, 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":
                    buf.append(prefix);
                    buf.append('=');
                    appendAttValue(buf, value.substring(idx + 1));
                    done = true;
                    break;
            }
        }
        if (!done) {
            buf.append('c');
            buf.append('=');
            appendAttValue(buf, value);
        }
    }


    public static class Parameters {

        private boolean withSpecialParseChars;

        private Parameters() {
        }

        public boolean withSpecialParseChars() {
            return withSpecialParseChars;
        }

        public Parameters withSpecialParseChars(boolean withSpecialParseChars) {
            this.withSpecialParseChars = withSpecialParseChars;
            return this;
        }

    }

}
