/* UtilLib - Copyright (c) 2009-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.mapeadores.util.attr;

import java.io.IOException;
import java.text.ParseException;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.RandomAccess;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
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.instruction.InstructionUtils;
import net.mapeadores.util.logging.LogUtils;
import net.mapeadores.util.logging.MessageHandler;
import net.mapeadores.util.text.CleanedString;
import net.mapeadores.util.text.LabelUtils;
import net.mapeadores.util.text.Labels;
import net.mapeadores.util.text.Phrases;
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 AttributeUtils {

    public final static List<AttributeKey> EMPTY_ATTRIBUTEKEYLIST = Collections.emptyList();
    public final static List<AttributeDef> EMPTY_ATTRIBUTEDEFLIST = Collections.emptyList();
    public final static AttributeChange EMPTY_ATTRIBUTECHANGE = new EmptyAttributeChange();
    public final static Attributes EMPTY_ATTRIBUTES = new EmptyAttributes();
    public final static Predicate<AttributeKey> NONE_PREDICATE = new BooleanPredicate(false);
    public final static Predicate<AttributeKey> ALL_PREDICATE = new BooleanPredicate(true);

    private AttributeUtils() {
    }

    public static boolean isEqual(Attribute attr1, Attribute attr2) {
        boolean test1 = attr1.getAttributeKey().equals(attr2.getAttributeKey());
        if (!test1) {
            return false;
        }
        int valueLength = attr1.size();
        if (valueLength != attr2.size()) {
            return false;
        }
        for (int i = 0; i < valueLength; i++) {
            if (!attr1.get(i).equals(attr2.get(i))) {
                return false;
            }
        }
        return true;
    }

    public static Attributes toSingletonAttributes(Attribute attribute) {
        if (attribute == null) {
            throw new IllegalArgumentException("attribute is null");
        }
        return new SingletonAttributes(attribute);
    }

    public static AttributeDef toDefaultAttributeDef(AttributeKey attributeKey) {
        if (attributeKey == null) {
            throw new IllegalArgumentException("attributeKey is null");
        }
        return new DefaultAttributeDef(attributeKey);
    }

    public static void addAttributes(XMLWriter xmlWriter, Attributes attributes) throws IOException {
        int length = attributes.size();
        for (int i = 0; i < length; i++) {
            addAttribute(xmlWriter, attributes.get(i));
        }
    }

    public static void readAttrElement(AttributeBuffer attributeBuffer, Element element) {
        readAttrElement(attributeBuffer, element, LogUtils.NULL_MESSAGEHANDLER, "", null);
    }

    public static void readAttrElement(AttributeBuffer attributeBuffer, Element element, AttributeKeyAlias attributeKeyAlias) {
        readAttrElement(attributeBuffer, element, LogUtils.NULL_MESSAGEHANDLER, "", attributeKeyAlias);
    }

    public static void readAttrElement(AttributeBuffer attributeBuffer, Element element, MessageHandler messageHandler, String xpath) {
        readAttrElement(attributeBuffer, element, messageHandler, xpath, null);
    }

    public static void readAttrElement(AttributeBuffer attributeBuffer, Element element, MessageHandler messageHandler, String xpath, AttributeKeyAlias attributeKeyAlias) {
        AttributeKey attributeKey = readAttributeKey(element, messageHandler, xpath, attributeKeyAlias);
        if (attributeKey == null) {
            return;
        }
        DOMUtils.readChildren(element, new AttributeConsumer(attributeKey, attributeBuffer, messageHandler, xpath + "[@ns='" + attributeKey.getNameSpace() + "' AND @key='" + attributeKey.getLocalKey() + "']"));
    }

    public static AttributeKey readAttributeKey(Element element) {
        return readAttributeKey(element, LogUtils.NULL_MESSAGEHANDLER, "");
    }

    public static AttributeKey readAttributeKey(Element element, MessageHandler messageHandler, String xpath) {
        return readAttributeKey(element, messageHandler, xpath, null);
    }

    public static AttributeKey readAttributeKey(Element element, MessageHandler messageHandler, String xpath, AttributeKeyAlias attributeKeyAlias) {
        String nameSpaceString = element.getAttribute("ns");
        if (nameSpaceString.length() == 0) {
            DomMessages.emptyAttribute(messageHandler, xpath, "ns");
            return null;
        }
        String localKeyString = element.getAttribute("key");
        if (localKeyString.length() == 0) {
            DomMessages.emptyAttribute(messageHandler, xpath, "key");
            return null;
        }
        xpath = xpath + "[@ns='" + nameSpaceString + "' AND @key='" + localKeyString + "']";
        if (attributeKeyAlias != null) {
            nameSpaceString = attributeKeyAlias.checkNameSpace(nameSpaceString);
            localKeyString = attributeKeyAlias.checkLocalKey(localKeyString);
        }
        CheckedNameSpace nameSpace;
        try {
            nameSpace = CheckedNameSpace.parse(nameSpaceString);
        } catch (ParseException pe) {
            DomMessages.wrongAttributeValue(messageHandler, xpath, "ns", nameSpaceString);
            return null;
        }
        CheckedLocalKey localKey;
        try {
            localKey = CheckedLocalKey.parse(localKeyString);
        } catch (ParseException pe) {
            DomMessages.wrongAttributeValue(messageHandler, xpath, "key", localKeyString);
            return null;
        }
        return AttributeKey.build(nameSpace, localKey);
    }

    public static List<AttributeDef> toImmutableList(Collection<AttributeDef> attributeDefs) {
        return wrap(attributeDefs.toArray(new AttributeDef[attributeDefs.size()]));
    }

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

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

    public static void parseInstruction(AttributeBuffer attributeBuffer, String s, CheckedNameSpace defaultNameSpace, InstructionErrorHandler errorHandler) {
        if (errorHandler == null) {
            errorHandler = InstructionUtils.DEFAULT_ERROR_HANDLER;
        }
        Instruction instruction = InstructionParser.parse(s, errorHandler);
        if (instruction == null) {
            return;
        }
        for (Argument argument : instruction) {
            CleanedString cleanedString = CleanedString.newInstance(argument.getValue());
            if (cleanedString != null) {
                try {
                    AttributeKey attributeKey = parse(argument.getKey(), defaultNameSpace);
                    attributeBuffer.appendValue(attributeKey, cleanedString);
                } catch (ParseException pe) {
                }
            }
        }
    }

    private static AttributeKey parse(String key, CheckedNameSpace defaultNameSpace) throws ParseException {
        if (defaultNameSpace == null) {
            return AttributeKey.parse(key);
        } else {
            CheckedLocalKey localKey = CheckedLocalKey.parse(key);
            return AttributeKey.build(defaultNameSpace, localKey);
        }
    }

    public static void addAttribute(XMLWriter xmlWriter, Attribute attribute) throws IOException {
        AttributeKey key = attribute.getAttributeKey();
        String nameSpace = key.getNameSpace();
        if (nameSpace.equals(AttributeKey.TRANSIENT_NAMESPACE)) {
            return;
        }
        xmlWriter.startOpenTag("attr")
                .addAttribute("ns", nameSpace)
                .addAttribute("key", key.getLocalKey())
                .endOpenTag();
        int valueLength = attribute.size();
        for (int i = 0; i < valueLength; i++) {
            xmlWriter.addSimpleElement("val", attribute.get(i));
        }
        xmlWriter.closeTag("attr");
    }

    public static Predicate<AttributeKey> toAttributeKeyPredicate(Collection<String> filters) {
        if (filters.isEmpty()) {
            return NONE_PREDICATE;
        }
        Set<String> equalSet = new HashSet<String>();
        List<String> prefixList = new ArrayList<String>();
        for (String filter : filters) {
            if (filter.equals("*")) {
                return ALL_PREDICATE;
            } else if (filter.endsWith("*")) {
                prefixList.add(filter.substring(0, filter.length() - 1));
            } else {
                equalSet.add(filter);
            }
        }
        return new FilterPredicate(equalSet, prefixList);
    }


    private static class EmptyAttributeChange implements AttributeChange {

        private EmptyAttributeChange() {
        }

        @Override
        public List<AttributeKey> getRemovedAttributeKeyList() {
            return EMPTY_ATTRIBUTEKEYLIST;
        }

        @Override
        public Attributes getChangedAttributes() {
            return EMPTY_ATTRIBUTES;
        }

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

    }


    private static class DefaultAttributeDef implements AttributeDef {

        private final AttributeKey attributeKey;

        private DefaultAttributeDef(AttributeKey attributeKey) {
            this.attributeKey = attributeKey;
        }

        @Override
        public AttributeKey getAttributeKey() {
            return attributeKey;
        }

        @Override
        public Object getDefObject(String objectName) {
            return null;
        }

        @Override
        public Labels getTitleLabels() {
            return LabelUtils.EMPTY_LABELS;
        }

        @Override
        public Phrases getPhrases() {
            return LabelUtils.EMPTY_PHRASES;
        }

        @Override
        public Attributes getAttributes() {
            return EMPTY_ATTRIBUTES;
        }

    }


    private static class AttributeConsumer implements Consumer<Element> {

        private final AttributeKey attributeKey;
        private final AttributeBuffer attributeBuffer;
        private final MessageHandler messageHandler;
        private final String xpath;

        public AttributeConsumer(AttributeKey attributeKey, AttributeBuffer attributesBuilder, MessageHandler messageHandler, String xpath) {
            this.attributeKey = attributeKey;
            this.attributeBuffer = attributesBuilder;
            this.messageHandler = messageHandler;
            this.xpath = xpath;
        }

        @Override
        public void accept(Element element) {
            String tagName = element.getTagName();
            String currentXpath = xpath + "/" + tagName;
            if (tagName.equals("val")) {
                CleanedString cs = XMLUtils.toCleanedString(element);
                if (cs != null) {
                    attributeBuffer.appendValue(attributeKey, cs);
                } else {
                    DomMessages.emptyElement(messageHandler, currentXpath);
                }
            } else {
                DomMessages.unknownTagWarning(messageHandler, currentXpath);
            }
        }

    }


    private static class AttributeDefList extends AbstractList<AttributeDef> implements RandomAccess {

        private final AttributeDef[] array;

        private AttributeDefList(AttributeDef[] array) {
            this.array = array;
        }

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

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

    }


    private static class AttributeKeyList extends AbstractList<AttributeKey> implements RandomAccess {

        private final AttributeKey[] array;

        private AttributeKeyList(AttributeKey[] array) {
            this.array = array;
        }

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

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

    }


    private static class EmptyAttributes extends AbstractList<Attribute> implements Attributes {

        private EmptyAttributes() {
        }

        @Override
        public Attribute getAttribute(AttributeKey key) {
            return null;
        }


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

        @Override
        public Attribute get(int i) {
            throw new IndexOutOfBoundsException();
        }

    }


    private static class SingletonAttributes extends AbstractList<Attribute> implements Attributes {

        private final Attribute attribute;

        private SingletonAttributes(Attribute attribute) {
            this.attribute = attribute;
        }

        @Override
        public Attribute getAttribute(AttributeKey key) {
            if (attribute.getAttributeKey().equals(key)) {
                return attribute;
            } else {
                return null;
            }
        }

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

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

    }


    private static class BooleanPredicate implements Predicate<AttributeKey> {

        private final boolean value;

        private BooleanPredicate(boolean value) {
            this.value = value;
        }

        @Override
        public boolean test(AttributeKey attributeKey) {
            return value;
        }

    }


    private static class FilterPredicate implements Predicate<AttributeKey> {

        private final Set<String> equalSet;
        private final List<String> prefixList;

        private FilterPredicate(Set<String> equalSet, List<String> startsWithList) {
            this.equalSet = equalSet;
            this.prefixList = startsWithList;
        }

        @Override
        public boolean test(AttributeKey attributeKey) {
            String text = attributeKey.toString();
            if (equalSet.contains(text)) {
                return true;
            }
            for (String prefix : prefixList) {
                if (text.startsWith(prefix)) {
                    return true;
                }
            }
            return false;
        }

    }

}
