/* UtilLib - Copyright (c) 2011-2024 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.mapeadores.util.text;

import java.io.IOException;
import java.text.ParseException;
import java.util.AbstractList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.RandomAccess;
import java.util.function.Consumer;
import net.mapeadores.util.instruction.Argument;
import net.mapeadores.util.instruction.Instruction;
import net.mapeadores.util.instruction.InstructionErrorHandler;
import net.mapeadores.util.instruction.InstructionParser;
import net.mapeadores.util.localisation.Lang;
import net.mapeadores.util.localisation.Langs;
import net.mapeadores.util.localisation.LangsUtils;
import net.mapeadores.util.logging.MessageHandler;
import net.mapeadores.util.misc.DefBuilder;
import net.mapeadores.util.xml.DOMUtils;
import net.mapeadores.util.xml.DomMessages;
import net.mapeadores.util.xml.XMLUtils;
import net.mapeadores.util.xml.XMLWriter;
import org.w3c.dom.Element;


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

    public final static Labels EMPTY_LABELS = new EmptyLabels();
    public final static LabelChange EMPTY_LABELCHANGE = new EmptyLabelChange();
    public final static Phrases EMPTY_PHRASES = new EmptyPhrases();


    private LabelUtils() {
    }

    public static boolean areEquals(Label label1, Label label2) {
        if (label1 == null) {
            return (label2 == null);
        } else if (label2 == null) {
            return false;
        }
        if (!label1.getLang().equals(label2.getLang())) {
            return false;
        }
        return label1.getLabelString().equals(label2.getLabelString());
    }

    /*
     * Retourne nul si vide.
     * si l'instruction est incorrecte, retourne un labelHolder vide avec un message d'erreur par défaut.
     * l'instruction doit être sous la forme fr="",en="", etc.
     */
    public static Labels parseInstruction(String s) throws ParseException {
        if (s == null) {
            return null;
        }
        s = s.trim();
        if (s.length() == 0) {
            return null;
        }
        if (s.indexOf('=') == -1) {
            throw new ParseException("#missingEqualCharacter", 0);
        }
        InstructionParseErrorHandler errorHandler = new InstructionParseErrorHandler();
        Instruction instruction = InstructionParser.parse(s, errorHandler);
        if (errorHandler.hasError()) {
            throw new ParseException(errorHandler.getErrorMessage(), 0);
        }
        if (instruction == null) {
            return null;
        }
        LabelChangeBuilder builder = new LabelChangeBuilder();
        for (Argument argument : instruction) {
            try {
                Lang lang = Lang.parse(argument.getKey());
                builder.putLabel(lang, argument.getValue());
            } catch (ParseException pe) {
            }
        }
        return builder.toLabels();
    }

    /**
     * Retourne nul si vide
     */
    public static Label readLabel(Element element) throws ParseException {
        Lang lang = Lang.parse(element.getAttribute("xml:lang"));
        CleanedString labelString = DOMUtils.contentToCleanedString(element);
        if (labelString == null) {
            return null;
        }
        return toLabel(lang, labelString);
    }

    public static void readLabel(Element element, DefBuilder defBuilder) throws ParseException {
        Label label = readLabel(element);
        if (label != null) {
            defBuilder.putLabel(label);
        }
    }

    public static void readLabel(Element element, LabelChangeBuilder labelChangeBuilder) throws ParseException {
        Lang lang = Lang.parse(element.getAttribute("xml:lang"));
        CleanedString labelString = DOMUtils.contentToCleanedString(element);
        if (labelString != null) {
            labelChangeBuilder.putLabel(toLabel(lang, labelString));
        } else {
            labelChangeBuilder.putRemovedLang(lang);
        }
    }


    public static Label toLabel(Lang lang, CleanedString cleanedString) {
        if (lang == null) {
            throw new IllegalArgumentException("lang is null");
        }
        if (cleanedString == null) {
            throw new IllegalArgumentException("cleanedString is null");
        }
        return new InternalLabel(lang, cleanedString.toString());
    }

    public static String seekLabelString(Phrases phrases, String phraseName, Lang lang, String defaultString) {
        Phrase phrase = phrases.getPhrase(phraseName);
        if (phrase == null) {
            return defaultString;
        }
        return phrase.seekLabelString(lang, defaultString);
    }

    public static Label getFirstLabel(Labels labels, Lang[] langArray) {
        for (Lang lang : langArray) {
            Label label = labels.getLabel(lang);
            if (label != null) {
                return label;
            }
        }
        return null;
    }

    public static Label getFirstLabel(Labels labels, Langs langs) {
        int length = langs.size();
        for (int i = 0; i < length; i++) {
            Lang lang = langs.get(i);
            Label label = labels.getLabel(lang);
            if (label != null) {
                return label;
            }
        }
        return null;
    }

    public static void addLabels(XMLWriter xmlWriter, Labels labels) throws IOException {
        addLabels(xmlWriter, labels, null);
    }

    public static void addLabels(XMLWriter xmlWriter, Labels labels, LangFilter langFilter) throws IOException {
        int length = labels.size();
        for (int i = 0; i < length; i++) {
            Label label = labels.get(i);
            if ((langFilter == null) || (langFilter.accept(label.getLang()))) {
                addLabel(xmlWriter, label.getLang(), label.getLabelString());
            }
        }
    }

    public static void addLabel(XMLWriter xmlWriter, Lang lang, String text) throws IOException {
        xmlWriter.startOpenTag("label");
        XMLUtils.addXmlLangAttribute(xmlWriter, lang);
        xmlWriter.endOpenTag();
        xmlWriter.addText(text);
        xmlWriter.closeTag("label", false);
    }

    public static void addPhrases(XMLWriter xmlWriter, Phrases phrases) throws IOException {
        for (Phrase phrase : phrases) {
            xmlWriter.startOpenTag("phrase");
            xmlWriter.addAttribute("name", phrase.getName());
            xmlWriter.endOpenTag();
            addLabels(xmlWriter, phrase, null);
            xmlWriter.closeTag("phrase");
        }
    }

    public static Labels toSingletonLabels(Label label) {
        if (label == null) {
            throw new IllegalArgumentException("label is null");
        }
        return new SingletonLabels(label);
    }

    public static Labels toLabels(Map<Lang, Label> labelMap) {
        int size = labelMap.size();
        switch (size) {
            case 0:
                return EMPTY_LABELS;
            case 1:
                return new SingletonLabels(labelMap.values().iterator().next());
            default:
                Label[] labelArray = labelMap.values().toArray(new Label[size]);
                if (size < 4) {
                    return new ArrayLabels(labelArray);
                } else {
                    return new MapLabels(new HashMap<Lang, Label>(labelMap), labelArray);
                }
        }

    }

    public static Phrase toPhrase(String name, Map<Lang, Label> labelMap) {
        if (name == null) {
            throw new IllegalArgumentException("name is null");
        }
        int size = labelMap.size();
        switch (size) {
            case 0:
                return new EmptyPhrase(name);
            case 1:
                return new SingletonPhrase(name, labelMap.values().iterator().next());
            default:
                Label[] labelArray = labelMap.values().toArray(new Label[size]);
                if (size < 4) {
                    return new ArrayPhrase(name, labelArray);
                } else {
                    return new MapPhrase(name, new HashMap<Lang, Label>(labelMap), labelArray);
                }
        }

    }

    public static void readPhraseElement(PhrasesBuilder phrasesBuilder, Element element, MessageHandler messageHandler, String xpath) {
        String name = element.getAttribute("name");
        if (name.length() == 0) {
            DomMessages.emptyAttribute(messageHandler, xpath, "name");
            return;
        }
        xpath = xpath + "[@name='" + name + "']";
        readPhraseElement(phrasesBuilder, element, messageHandler, xpath, name);
    }

    public static void readPhraseElement(PhrasesBuilder phrasesBuilder, Element element, MessageHandler messageHandler, String xpath, String name) {
        LabelChangeBuilder phraseBuilder = phrasesBuilder.getPhraseBuilder(name);
        DOMUtils.readChildren(element, new PhraseConsumer(phraseBuilder, messageHandler, xpath));
    }


    private static class EmptyLabels extends AbstractList<Label> implements Labels {

        private EmptyLabels() {

        }

        @Override
        public int size() {
            return 0;
        }

        @Override
        public Label get(int i) {
            throw new IndexOutOfBoundsException("langCount = 0");
        }

        @Override
        public Label getLabel(Lang lang) {
            return null;
        }

    }


    private static class EmptyPhrase extends EmptyLabels implements Phrase {

        private final String name;

        private EmptyPhrase(String name) {
            this.name = name;
        }

        @Override
        public String getName() {
            return name;
        }

    }


    private static class SingletonLabels extends AbstractList<Label> implements Labels {

        private final Label label;

        private SingletonLabels(Label label) {
            this.label = label;
        }

        @Override
        public int size() {
            return 1;
        }

        @Override
        public Label get(int i) {
            if (i != 0) {
                throw new IndexOutOfBoundsException();
            }
            return label;
        }

        @Override
        public Label getLabel(Lang lang) {
            if (lang.equals(label.getLang())) {
                return label;
            } else {
                return null;
            }
        }

    }


    private static class SingletonPhrase extends SingletonLabels implements Phrase {

        private final String name;

        private SingletonPhrase(String name, Label label) {
            super(label);
            this.name = name;
        }

        @Override
        public String getName() {
            return name;
        }

    }


    private static class MapLabels extends AbstractList<Label> implements Labels {

        private final Label[] labelArray;
        private final Map<Lang, Label> labelMap;

        private MapLabels(Map<Lang, Label> labelMap, Label[] labelArray) {
            this.labelArray = labelArray;
            this.labelMap = labelMap;
        }

        @Override
        public Label getLabel(Lang lang) {
            return labelMap.get(lang);
        }

        @Override
        public int size() {
            return labelArray.length;
        }

        @Override
        public Label get(int index) {
            return labelArray[index];
        }

    }


    private static class MapPhrase extends MapLabels implements Phrase {

        private final String name;

        private MapPhrase(String name, Map<Lang, Label> labelMap, Label[] labelArray) {
            super(labelMap, labelArray);
            this.name = name;
        }

        @Override
        public String getName() {
            return name;
        }

    }


    private static class ArrayLabels extends AbstractList<Label> implements Labels {

        private final Label[] labelArray;

        private ArrayLabels(Label[] labelArray) {
            this.labelArray = labelArray;
        }

        @Override
        public Label getLabel(Lang lang) {
            for (Label label : labelArray) {
                if (label.getLang().equals(lang)) {
                    return label;
                }
            }
            return null;
        }

        @Override
        public int size() {
            return labelArray.length;
        }

        @Override
        public Label get(int index) {
            return labelArray[index];
        }

    }


    private static class ArrayPhrase extends ArrayLabels implements Phrase {

        private final String name;

        private ArrayPhrase(String name, Label[] labelArray) {
            super(labelArray);
            this.name = name;
        }

        @Override
        public String getName() {
            return name;
        }

    }


    private static class EmptyLabelChange implements LabelChange {

        private EmptyLabelChange() {
        }

        @Override
        public Labels getChangedLabels() {
            return EMPTY_LABELS;
        }

        @Override
        public List<Lang> getRemovedLangList() {
            return LangsUtils.EMPTY_LANGLIST;
        }

        @Override
        public boolean isEmpty() {
            return true;
        }

    }


    public static class InstructionParseErrorHandler implements InstructionErrorHandler {

        private String errorMessage;

        private InstructionParseErrorHandler() {

        }

        @Override
        public void invalidAsciiCharacterError(String part, int row, int col) {
            errorMessage = "#invalidAsciiCharacter: " + part + " (" + row + "," + col + ")#";
        }

        @Override
        public void invalidEndCharacterError(String part, int row, int col) {
            errorMessage = "#invalidEndCharacter: " + part + " (" + row + "," + col + ")#";
        }

        @Override
        public void invalidSeparatorCharacterError(String part, int row, int col) {
            errorMessage = "#invalidSeparatorCharacter: " + part + " (" + row + "," + col + ")#";
        }

        private boolean hasError() {
            return (errorMessage != null);
        }

        private String getErrorMessage() {
            return errorMessage;
        }

    }


    private static class InternalLabel implements Label {

        private final Lang lang;
        private final String labelString;

        private InternalLabel(Lang lang, String labelString) {
            this.lang = lang;
            this.labelString = labelString;
        }

        @Override
        public Lang getLang() {
            return lang;
        }

        @Override
        public String getLabelString() {
            return labelString;
        }

    }


    private static class EmptyPhrases extends AbstractList<Phrase> implements Phrases {

        private EmptyPhrases() {

        }

        @Override
        public int size() {
            return 0;
        }

        @Override
        public Phrase get(int index) {
            throw new IndexOutOfBoundsException();
        }

        @Override
        public Phrase getPhrase(String name) {
            return null;
        }

    }

    public static List<Label> wrap(Label[] array) {
        return new LabelList(array);
    }


    private static class LabelList extends AbstractList<Label> implements RandomAccess {

        private final Label[] array;

        private LabelList(Label[] array) {
            this.array = array;
        }

        @Override
        public int size() {
            return array.length;
        }

        @Override
        public Label get(int index) {
            return array[index];
        }

    }

    public static List<Phrase> wrap(Phrase[] array) {
        return new PhraseList(array);
    }


    private static class PhraseList extends AbstractList<Phrase> implements RandomAccess {

        private final Phrase[] array;

        private PhraseList(Phrase[] array) {
            this.array = array;
        }

        @Override
        public int size() {
            return array.length;
        }

        @Override
        public Phrase get(int index) {
            return array[index];
        }

    }


    private static class PhraseConsumer implements Consumer<Element> {

        private final LabelChangeBuilder phraseBuilder;
        private final MessageHandler messageHandler;
        private final String xpath;

        private PhraseConsumer(LabelChangeBuilder phraseBuilder, MessageHandler messageHandler, String xpath) {
            this.phraseBuilder = phraseBuilder;
            this.messageHandler = messageHandler;
            this.xpath = xpath;
        }

        @Override
        public void accept(Element element) {
            String tagName = element.getTagName();
            String tagPath = xpath + "/" + tagName;
            if (tagName.equals("label")) {
                String langString = element.getAttribute("xml:lang");
                if (langString.isEmpty()) {
                    DomMessages.emptyAttribute(messageHandler, tagPath, "xml:lang");
                } else {
                    tagPath = tagPath + "[@xml:lang='" + langString + "']";
                    try {
                        Lang lang = Lang.parse(element.getAttribute("xml:lang"));
                        CleanedString labelString = DOMUtils.contentToCleanedString(element);
                        if (labelString != null) {
                            phraseBuilder.putLabel(lang, labelString);
                        } else {
                            DomMessages.emptyElement(messageHandler, tagPath);
                        }
                    } catch (ParseException pe) {
                        DomMessages.wrongLangAttribute(messageHandler, tagPath, langString);
                    }
                }
            } else {
                DomMessages.unknownTagWarning(messageHandler, tagPath);
            }
        }

    }

}
