/*-
 * ~~~~~~licensing~~~~~~
 * object-producer-core
 * ==========
 * Copyright (C) 2020 - 2025 EmDev LLC
 * ==========
 * You may not use this file except in accordance with the License Terms of the Copyright
 * Holder located at: https://entaxy.ru/eula . All copyrights, all intellectual property
 * rights to the Software and any copies are the property of the Copyright Holder. Unless
 * it is explicitly allowed the Copyright Holder, the User is prohibited from using the
 * Software for commercial purposes to provide services to third parties.
 * 
 * The Copyright Holder hereby declares that the Software is provided on an "AS IS".
 * Under no circumstances does the Copyright Holder guarantee or promise that the
 * Software provided by him will be suitable or not suitable for the specific purposes
 * of the User, that the Software will meet all commercial and personal subjective
 * expectations of the User, that the Software will work properly, without technical
 * errors, quickly and uninterruptedly.
 * 
 * Under no circumstances shall the Copyright Holder or its Affiliates is not liable
 * to the User for any direct or indirect losses of the User, his expenses or actual
 * damage, including, downtime; loss of bussines; lost profit; lost earnings; loss
 * or damage to data, property, etc.
 * ~~~~~~/licensing~~~~~~
 */
package ru.entaxy.platform.core.producer.executor.generationmodel.merge;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;

import org.osgi.framework.Bundle;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import ru.entaxy.esb.platform.runtime.base.connecting.generator.Generated;
import ru.entaxy.esb.platform.runtime.base.extensions.api.EntaxyExtension;
import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.JSONUtils;
import ru.entaxy.platform.base.support.xml.CommonXMLUtils;
import ru.entaxy.platform.core.producer.executor.generationmodel.merge.MergingMap.Mapping;
import ru.entaxy.platform.core.producer.executor.support.EntaxyObjectPropertiesHelper;

@Component(service = MergeProcessor.class, immediate = true)
@MergingMap(mappings = {
    @Mapping(source = Generated.GENERATED_TYPE_BLUEPRINT_FRAGMENT,
            targets = {Generated.GENERATED_TYPE_BLUEPRINT, Generated.GENERATED_TYPE_BLUEPRINT_FRAGMENT})
})
public class BlueprintMergeProcessor extends AbstractMergeProcessor {

    private static final Logger log = LoggerFactory.getLogger(BlueprintMergeProcessor.class);

    public static final String ENTAXY_EXTENSION_PATH = "merge/config";
    public static final String ENTAXY_EXTENSION_NAME = "BlueprintMergeProcessor.json";

    public static final String ROOT_NODE_NAME = "##root";

    public static final String FRAGMENT_NAME = "fragment";
    public static final String FRAGMENT_START = "<" + FRAGMENT_NAME + ">";
    public static final String FRAGMENT_END = "</" + FRAGMENT_NAME + ">";

    protected MergeExecutor defaultExecutor;

    @Activate
    public void activate(ComponentContext componentContext) {
        Bundle bundle = componentContext.getBundleContext().getBundle();
        URL url = bundle.getEntry("/config/".concat(ENTAXY_EXTENSION_NAME));
        defaultExecutor = new MergeExecutor();
        try (InputStream is = url.openStream()) {
            defaultExecutor.load(is);
        } catch (Exception e) {
            log.error("Error loading configuration", e);
            defaultExecutor.loadDefaults();
        }
    }

    @Override
    public Generated merge(Generated source, Generated target, Map<String, Object> config) {

        MergeExecutor currentExecutor;

        String mergeConfig = config == null
                ? ""
                : config.getOrDefault("merge.config", "").toString();
        if (CommonUtils.isValid(mergeConfig)) {
            currentExecutor = new MergeExecutor();
            try {
                currentExecutor.load(mergeConfig);
            } catch (Exception e) {
                log.error("Invalid merge.config: \n" + mergeConfig, e);
                currentExecutor = defaultExecutor.createCopy();
            }
        } else {
            currentExecutor = defaultExecutor.createCopy();

            // append configuration from extensions
            final List<URL> urlsToAppend = new ArrayList<>();
            for (EntaxyExtension ext : entaxyExtensions) {
                ext.getResources().entrySet().stream()
                        .filter(entry -> entry.getKey().startsWith(ENTAXY_EXTENSION_PATH)
                                && entry.getValue().toString().endsWith(ENTAXY_EXTENSION_NAME))
                        .map(entry -> entry.getValue())
                        .forEach(url -> urlsToAppend.add(url));
            }
            for (URL url : urlsToAppend) {
                try {
                    JsonObject jsonObject = JSONUtils.getJsonRootObject(url);
                    currentExecutor.config.addConfig(jsonObject);
                } catch (IOException e) {
                    log.error("Erorr loading resource: [" + url + "]", e);
                }
            }
        }

        NodeList sourceNodes = null;
        if (source.getType().equals(Generated.GENERATED_TYPE_BLUEPRINT_FRAGMENT)) {
            // remove <?xml .. ?> if present
            String sourceValue = source.getObject().toString().trim();
            if (sourceValue.startsWith("<?xml")) {
                int index = sourceValue.indexOf("?>");
                sourceValue = sourceValue.substring(index + 2);
            }
            String toParse = FRAGMENT_START + sourceValue + FRAGMENT_END;
            try {
                Document sourceDoc = CommonXMLUtils.parseString(true, toParse);
                sourceNodes = sourceDoc.getFirstChild().getChildNodes();
                for (int i = 0; i < sourceNodes.getLength(); i++) {
                    Node node = sourceNodes.item(i);
                }
            } catch (Exception e) {
                log.error("Can't parse: \n" + toParse, e);
            }
        }

        if ((sourceNodes == null) || (sourceNodes.getLength() == 0))
            return target;

        if (target.getType().equals(Generated.GENERATED_TYPE_BLUEPRINT)) {
            return mergeToBlueprint(sourceNodes, source, target, currentExecutor);
        }

        if (target.getType().equals(Generated.GENERATED_TYPE_BLUEPRINT_FRAGMENT)) {
            return mergeToFragment(sourceNodes, source, target, currentExecutor);
        }

        return null;
    }

    protected Generated mergeToBlueprint(NodeList sourceNodes, Generated source, Generated target,
            MergeExecutor currentProcessor) {
        Document targetDoc = null;
        Node targetNode = null;
        Node targetNodeBefore = null;

        try {
            targetDoc = CommonXMLUtils.parseString(true, target.getObject().toString());
            targetNode = targetDoc.getFirstChild();

            currentProcessor.setDocument(targetDoc);


        } catch (Exception e) {
            log.error("Can't parse: \n" + target.getObject().toString(), e);
        }

        if (!currentProcessor.hasDocument())
            return null;

        currentProcessor.reset();

        /*for (int i=0; i<sourceNodes.getLength(); i++)
        	currentProcessor.mergeNode(sourceNodes.item(i));
        */

        currentProcessor.mergeNodes(sourceNodes);

        try {
            String resultBlueprint = CommonXMLUtils.doc2string(targetDoc);
            Generated result = Generated.create(Generated.GENERATED_TYPE_BLUEPRINT, resultBlueprint);
            result.getProperties().putAll(
                    EntaxyObjectPropertiesHelper.merge(source.getProperties(), target.getProperties()));
            return result;
        } catch (Exception e) {
            log.error("Can't convert to string", e);
        }

        return null;
    }

    protected Generated mergeToFragment(NodeList sourceNodes, Generated source, Generated target,
            MergeExecutor currentProcessor) {

        // remove <?xml .. ?> if present
        String targetValue = target.getObject().toString().trim();
        if (targetValue.startsWith("<?xml")) {
            int index = targetValue.indexOf("?>");
            targetValue = targetValue.substring(index + 2);
        }
        String toParse = FRAGMENT_START + targetValue + FRAGMENT_END;

        Document targetDoc = null;

        try {
            targetDoc = CommonXMLUtils.parseString(true, toParse);
            for (int i = 0; i < sourceNodes.getLength(); i++) {
                targetDoc.getFirstChild().appendChild(
                        targetDoc.adoptNode(sourceNodes.item(i).cloneNode(true)));
            }
        } catch (Exception e) {
            log.error("Can't parse: \n" + toParse, e);
        }

        try {
            String resultFragment = CommonXMLUtils.doc2string(targetDoc).trim();

            if (resultFragment.startsWith(FRAGMENT_START))
                resultFragment = resultFragment.substring(FRAGMENT_START.length());
            if (resultFragment.endsWith(FRAGMENT_END))
                resultFragment = resultFragment.substring(0, resultFragment.length() - FRAGMENT_END.length());



            Generated result = Generated.create(Generated.GENERATED_TYPE_BLUEPRINT_FRAGMENT, resultFragment);
            result.getProperties().putAll(
                    EntaxyObjectPropertiesHelper.merge(source.getProperties(), target.getProperties()));
            return result;
        } catch (Exception e) {
            log.error("Can't convert to string", e);
        }


        return null;
    }

    protected static class MergeExecutor {

        public static String DEFAULT_CONFIG = "{\"*\":{}}";

        MergeProcessorConfig config = new MergeProcessorConfig();

        Document document;

        Node rootNode;
        Node manifestNode = null;

        boolean trackOrder = true;
        List<Node> processedNodes = new ArrayList<>();

        public void load(InputStream inputStream) throws IOException {
            log.info("loading config");
            String text = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
            inputStream.close();
            load(text);
        }

        public void loadDefaults() {
            load(DEFAULT_CONFIG);
        }

        public void load(String jsonData) {
            config.load(JSONUtils.getJsonRootObject(jsonData));
        }

        public MergeExecutor createCopy() {
            MergeExecutor result = new MergeExecutor();
            result.config.load(this.config.config);
            return result;
        }

        public void reset() {
            processedNodes.clear();
        }

        public Document getDocument() {
            return document;
        }

        public void setDocument(Document document) {
            this.document = document;

            rootNode = this.document.getFirstChild();

            NodeList manifests = this.document.getElementsByTagName("manifest");
            for (int i = 0; i < manifests.getLength(); i++)
                if ((manifests.item(i).getParentNode() != null) && (manifests.item(i).getParentNode() == rootNode)) {
                    manifestNode = manifests.item(i);
                    break;
                }

        }

        public boolean hasDocument() {
            return this.document != null;
        }

        protected NodeConflict checkNodeConflict(Node newNode, Node targetContainerNode, MERGE_POSITION position,
                MergeConfig mergeConfig) {


            if ((mergeConfig.unique == null) || mergeConfig.unique.isEmpty())
                return null;

            log.debug("\n[CHECK CONFLICT]: {}:{} -> {}:{}", newNode.getLocalName(), newNode.getNamespaceURI(),
                    targetContainerNode.getLocalName(), targetContainerNode.getNamespaceURI());

            String nodeName = newNode.getLocalName();
            String nodeNamspace = newNode.getNamespaceURI();
            Map<String, String> attributes = new HashMap<>();
            for (String attrName : mergeConfig.unique) {
                Node attr = newNode.getAttributes().getNamedItem(attrName);
                if (attr == null)
                    attributes.put(attrName, null);
                else
                    attributes.put(attrName, attr.getNodeValue());
            }

            NodeConflict result = null;

            NodeList testNodes = targetContainerNode.getChildNodes();
            for (int i = 0; i < testNodes.getLength(); i++) {
                Node item = testNodes.item(i);
                if (!nodeName.equalsIgnoreCase(item.getLocalName()))
                    continue;
                if (!mergeConfig.isIgnoreNamespace())
                    if ((nodeNamspace != null) && !nodeNamspace.equalsIgnoreCase(item.getNamespaceURI()))
                        continue;
                boolean found = false;
                for (Entry<String, String> attributeEntry : attributes.entrySet()) {
                    String attrValue = attributeEntry.getValue();
                    Node testAttr = item.getAttributes().getNamedItem(attributeEntry.getKey());
                    if ((testAttr == null) && (attrValue != null)) {
                        found = false;
                        break;
                    }
                    if ((testAttr != null) && (attrValue == null)) {
                        found = false;
                        break;
                    }
                    if ((testAttr != null) && (attrValue != null)) {
                        String testValue = testAttr.getNodeValue();
                        if (testValue == null) {
                            found = false;
                            break;
                        }
                        if (testValue.equalsIgnoreCase(attrValue)) {
                            found = true;
                        }
                    }
                }
                if (found) {
                    result = new NodeConflict();
                    result.newNode = newNode;
                    result.oldNode = item;
                    result.conflictResolution =
                            CommonUtils.getValid(mergeConfig.conflict, MergeConfig.CONFLICT_INGORE);
                    if (mergeConfig.conflicts == null)
                        result.conflictResolutionList = new ArrayList<>();
                    else
                        result.conflictResolutionList = new ArrayList<>(mergeConfig.conflicts);
                    break;
                }
            }

            if (result != null) {
                log.debug("\n[CONFLICT FOUND]: {}:{} -> {}:{}", newNode.getLocalName(), newNode.getNamespaceURI(),
                        targetContainerNode.getLocalName(), targetContainerNode.getNamespaceURI());
                log.debug("\n[RESOLUTIONS]: {}",
                        mergeConfig.conflicts != null && mergeConfig.conflicts.size() > 0
                                ? mergeConfig.conflicts.toString()
                                : mergeConfig.conflict);
            } else {
                log.debug("\n[NO CONFLICT]");
            }

            return result;
        }

        public List<Node> mergeNodes(NodeList sourceNodes) {
            List<Node> result = new ArrayList<>();

            mergeFilteredNodes(rootNode, sourceNodes, false, result);

            return result;
        }

        protected void mergeFilteredNodes(Node defaultTargetNode, NodeList sourceNodes, boolean recoursive,
                List<Node> toReturn) {
            for (int i = 0; i < sourceNodes.getLength(); i++) {
                Node current = sourceNodes.item(i);
                MergeConfig mergeConfig = this.config.getConfig(current);
                if (mergeConfig.isIgnore())
                    continue;
                if (mergeConfig.isTransparent()) {
                    mergeFilteredNodes(defaultTargetNode, current.getChildNodes(), recoursive, toReturn);
                    continue;
                }
                toReturn.add(this.mergeNode(defaultTargetNode, recoursive, current));
            }
        }

        public Node mergeNode(Node defaultTargetNode, boolean recoursive, Node sourceNode) {

            MergeConfig mergeConfig = config.getConfig(sourceNode);
            Node targetNode = null;
            if (CommonUtils.isValid(mergeConfig.targetNodeName)) {

                if (ROOT_NODE_NAME.equalsIgnoreCase(mergeConfig.targetNodeName))
                    targetNode = rootNode;
                else {
                    String[] nodePath = mergeConfig.targetNodeName.split("/");
                    Node currentNode = rootNode;
                    for (int i = 0; i < nodePath.length; i++) {
                        Node nextNode = null;
                        NodeList list = currentNode.getChildNodes();
                        for (int j = 0; j < list.getLength(); j++) {
                            Node current = list.item(j);
                            if (!nodePath[i].equalsIgnoreCase(current.getLocalName()))
                                continue;
                            if (CommonUtils.isValid(mergeConfig.targetNodeNamespace)) {
                                if (!mergeConfig.targetNodeNamespace.equalsIgnoreCase(current.getNamespaceURI()))
                                    continue;
                            }
                            nextNode = current;
                            break;
                        }
                        currentNode = nextNode;
                        if (currentNode == null)
                            break;
                    }
                    targetNode = currentNode;
                }
            } else {
                targetNode = defaultTargetNode;
            }

            if (targetNode == null) {
                if (mergeConfig.createTargetNode) {
                    if (!CommonUtils.isValid(mergeConfig.targetNodeXML)) {
                        log.warn("Can't merge node [{}:{}] : target node not found [{} : {}] and targetNodeXML not set",
                                sourceNode.getLocalName(), sourceNode.getNamespaceURI(), mergeConfig.targetNodeName,
                                mergeConfig.targetNodeNamespace);
                    } else {
                        try {
                            Document d = CommonXMLUtils.parseString(true, mergeConfig.targetNodeXML);
                            Node n = d.getFirstChild();
                            targetNode = this.mergeNode(defaultTargetNode, false, n);
                        } catch (Exception e) {
                            log.error("Error creating node from XML:\n" + mergeConfig.targetNodeXML, e);
                        }
                    }
                } else {
                    log.warn("Can't merge node [{}:{}] : target node not found [{} : {}]", sourceNode.getLocalName(),
                            sourceNode.getNamespaceURI(), mergeConfig.targetNodeName, mergeConfig.targetNodeNamespace);
                    return null;
                }
            }

            if (targetNode == null) {
                log.warn("Can't merge node [{}:{}] : target node not found [{} : {}]", sourceNode.getLocalName(),
                        sourceNode.getNamespaceURI(), mergeConfig.targetNodeName, mergeConfig.targetNodeNamespace);
                return null;
            }

            if (mergeConfig.targetAttributeName != null) {
                ((Element) targetNode).setAttribute(sourceNode.getNodeName(), sourceNode.getTextContent());
                return null;
            }

            if (mergeConfig.position == null)
                mergeConfig.position = MERGE_POSITION.INSIDE_FIRST.label;

            boolean effectiveRecoursive = mergeConfig.isRecoursiveSet() ? mergeConfig.isRecoursive() : recoursive;

            Node adopted = this.document.adoptNode(sourceNode.cloneNode(!effectiveRecoursive));
            if (targetNode == rootNode)
                // we can't add nodes before or after a root node
                if (mergeConfig.position.equals(MERGE_POSITION.AFTER.label)
                        || mergeConfig.position.equals(MERGE_POSITION.BEFORE.label))
                    mergeConfig.position = MERGE_POSITION.INSIDE_FIRST.label;

            Node currentRef;
            MERGE_POSITION position = MERGE_POSITION.valueOfLabel(mergeConfig.position);

            NodeConflict nodeConflict;
            switch (position) {
                case AFTER:
                case BEFORE:
                    nodeConflict = checkNodeConflict(adopted, targetNode.getParentNode(), position, mergeConfig);
                    break;
                default:
                    nodeConflict = checkNodeConflict(adopted, targetNode, position, mergeConfig);
            }

            if (nodeConflict != null) {

                log.debug("\n[CONFLICT TO RESOLVE]: {}:{} -> {}:{}", nodeConflict.newNode.getLocalName(),
                        nodeConflict.newNode.getNamespaceURI(),
                        nodeConflict.newNode.getLocalName(), nodeConflict.newNode.getNamespaceURI());

                if (log.isDebugEnabled()) {
                    try {
                        log.debug("\nOLD NODE:\n{}\nNEW NODE:{}", CommonXMLUtils.node2string(nodeConflict.oldNode),
                                CommonXMLUtils.node2string(nodeConflict.newNode));
                    } catch (Exception e) {
                        log.debug("ERROR :: " + e.getMessage());
                    }
                }
                List<String> conflictResolutionList = new ArrayList<>(nodeConflict.conflictResolutionList);
                if (conflictResolutionList.isEmpty())
                    conflictResolutionList.add(nodeConflict.conflictResolution);
                String primaryConflictResolution = conflictResolutionList.get(0);

                log.debug("\n[CONFLICT RESOLUTIONS]: {}", conflictResolutionList);
                if (MergeConfig.CONFLICT_REPLACE.equalsIgnoreCase(primaryConflictResolution)) {
                    // replace node
                    nodeConflict.oldNode.getParentNode().replaceChild(
                            targetNode.getOwnerDocument().adoptNode(nodeConflict.newNode.cloneNode(true)),
                            nodeConflict.oldNode);
                } else if (MergeConfig.CONFLICT_MERGE.equalsIgnoreCase(primaryConflictResolution)) {
                    log.debug("\n[CONFLICT RESOLVING]: merge");
                    String attributeConflictResolution =
                            conflictResolutionList.size() > 1 ? conflictResolutionList.get(1)
                                    : MergeConfig.CONFLICT_INGORE;
                    log.debug("\n[CONFLICT RESOLVING]: merge/{}", attributeConflictResolution);
                    for (int i = 0; i < nodeConflict.newNode.getAttributes().getLength(); i++) {
                        Node attributeNode = nodeConflict.newNode.getAttributes().item(i);
                        String attrName = attributeNode.getLocalName();
                        String attrValue = attributeNode.getNodeValue();

                        if (nodeConflict.oldNode.getAttributes().getNamedItem(attrName) == null) {
                            // add attribute
                            log.debug("\n[ADD ATTRIBUTE]: {}/{}", attrName, attrValue);
                            ((Element) nodeConflict.oldNode).setAttribute(attrName, attrValue);
                        } else {
                            if (MergeConfig.CONFLICT_REPLACE.equalsIgnoreCase(attributeConflictResolution)) {
                                // replace value
                                log.debug("\n[REPLACE ATTRIBUTE]: {}/{}", attrName, attrValue);
                                ((Element) nodeConflict.oldNode).setAttribute(attrName, attrValue);
                            } else {
                                // ignore
                            }
                        }
                    }

                    // switch adopted to old node to be target for further recursive merge
                    adopted = nodeConflict.oldNode;

                } else {
                    // ignore
                }
            } else {

                try {
                    switch (position) {
                        case AFTER:
                            currentRef = targetNode.getNextSibling();

                            if (trackOrder)
                                while ((currentRef != null) && processedNodes.contains(currentRef))
                                    currentRef = currentRef.getNextSibling();

                            if (currentRef != null)
                                targetNode.getParentNode().insertBefore(adopted, currentRef);
                            else
                                targetNode.getParentNode().appendChild(adopted);
                            break;
                        case BEFORE:
                            targetNode.getParentNode().insertBefore(adopted, targetNode);
                            break;
                        case INSIDE_FIRST:
                            currentRef = null;

                            if ((targetNode == rootNode) && (manifestNode != null))
                                // we assume 'first' as 'first after MANIFEST'
                                currentRef = manifestNode.getNextSibling();
                            else
                                currentRef = targetNode.getFirstChild();

                            if (trackOrder)
                                while ((currentRef != null) && processedNodes.contains(currentRef))
                                    currentRef = currentRef.getNextSibling();

                            if (currentRef == null)
                                targetNode.appendChild(adopted);
                            else
                                targetNode.insertBefore(adopted, currentRef);

                            break;
                        case INSIDE_LAST:
                            targetNode.appendChild(adopted);
                            break;
                        default:
                            return null;
                    }
                } catch (Exception e) {
                    log.error("Error merging node", e);
                    return null;
                }

            }

            if (trackOrder)
                this.processedNodes.add(adopted);

            if (effectiveRecoursive) {
                mergeFilteredNodes(adopted, sourceNode.getChildNodes(), true, processedNodes);

                if (!mergeConfig.childrenOrder.isEmpty())
                    reorderNode(adopted, mergeConfig);
            }

            return adopted;
        }

        protected void reorderNode(Node node, MergeConfig mergeConfig) {
            NodeSorter nodeSorter = new NodeSorter(node, mergeConfig);
            nodeSorter.sort();
        }
    }

    protected static class NodeSorter {

        Node node;
        MergeConfig mergeConfig;

        List<SortGroup> orderedGroupsBeforeAny = new LinkedList<>();
        List<SortGroup> orderedGroupsAny = new LinkedList<>();
        List<SortGroup> orderedGroupsAfterAny = new LinkedList<>();
        List<Node> nodesToProcess = new LinkedList<>();

        public NodeSorter(Node node, MergeConfig mergeConfig) {
            this.node = node;
            this.mergeConfig = mergeConfig;
        }

        public void sort() {

            List<SortGroup> groupsToSort = new LinkedList<>();

            SortGroup currentGroup = new SortGroup();

            for (int i = 0; i < node.getChildNodes().getLength(); i++) {

                currentGroup.addNode(node.getChildNodes().item(i));

                if (currentGroup.closed) {
                    groupsToSort.add(currentGroup);
                    currentGroup = new SortGroup();
                }

            }

            Map<String, List<SortGroup>> map = new LinkedHashMap<>();

            List<String> patterns = new LinkedList<>();

            // order nodes before and after *
            for (String nodeData : mergeConfig.childrenOrder) {
                int index = nodeData.indexOf(":");
                String nodeName = null;
                String nodeNamespace = null;

                if (index <= 0)
                    nodeName = nodeData;
                else {
                    nodeName = nodeData.substring(0, index);
                    try {
                        nodeNamespace = nodeData.substring(index + 1);
                    } catch (Exception ignore) {
                        // ignore
                    }
                }

                if ("*".equals(nodeName)) {
                    map.put(nodeData, new LinkedList<>());
                    patterns.add(nodeData);
                    continue;
                }

                final String _name = nodeName;
                final String _namespace = nodeNamespace;

                List<SortGroup> filtered =
                        groupsToSort.stream().filter(g -> _name.equalsIgnoreCase(g.mainNode.getLocalName()))
                                .filter(g -> CommonUtils.isValid(_namespace)
                                        ? (_namespace.equals(g.mainNode.getNamespaceURI()))
                                        : true)
                                .collect(Collectors.toList());

                if (filtered != null) {
                    map.put(nodeData, filtered);
                    groupsToSort.removeAll(filtered);
                }

            }

            // process patterns
            for (String pattern : patterns) {

                if (groupsToSort.isEmpty())
                    break;

                int index = pattern.indexOf(":");
                String nodeName = null;
                String nodeNamespace = null;

                if (index > 0) {
                    try {
                        nodeNamespace = pattern.substring(index + 1);
                    } catch (Exception ignore) {
                        // ignore
                    }
                }

                final String _namespace = nodeNamespace;

                List<SortGroup> filtered = groupsToSort.stream()
                        .filter(g -> _namespace == null ? true : _namespace.equals(g.mainNode.getNamespaceURI()))
                        .collect(Collectors.toList());

                if (!filtered.isEmpty()) {
                    map.get(pattern).addAll(filtered);
                    groupsToSort.removeAll(filtered);
                }

            }

            // remove all children
            while (node.hasChildNodes())
                node.removeChild(node.getFirstChild());

            for (String key : map.keySet()) {
                List<SortGroup> list = map.get(key);
                if (!list.isEmpty())
                    for (SortGroup group : list) {
                        if (!group.allNodes.isEmpty())
                            for (Node n : group.allNodes) {
                                node.appendChild(node.getOwnerDocument().adoptNode(n.cloneNode(true)));
                            }
                    }
            }



        }

        protected static class SortGroup {

            Node mainNode;
            List<Node> allNodes = new LinkedList<>();

            boolean closed = false;

            public boolean addNode(Node node) {
                allNodes.add(node);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    mainNode = node;
                    closed = true;
                }
                return closed;
            }

        }

    }

    protected static class NodeConflict {

        public Node newNode;
        public Node oldNode;

        public String conflictResolution;
        public List<String> conflictResolutionList = new ArrayList<>();

    }

    protected static class MergeProcessorConfig {

        JsonObject config;

        MergeConfig DEFAULT_CONFIG = new MergeConfig() {
            {
                // rootNode
                targetNodeName = null;
                // with any namespace
                targetNodeNamespace = null;
                // add to the end
                position = MERGE_POSITION.INSIDE_FIRST.label;
            }
        };

        public void load(JsonObject object) {
            this.config = object.deepCopy();
        }

        public void addConfig(JsonObject object) {
            JSONUtils.mergeObjects(object.deepCopy(), this.config);
        }

        public MergeConfig getConfig(Node sourceNode) {
            String name = sourceNode.getLocalName();
            String namespace = sourceNode.getNamespaceURI();
            JsonElement configElement = null;

            if (config.has(name + ":" + namespace)) {
                configElement = config.get(name + ":" + namespace);
            } else {
                if (config.has(name)) {
                    configElement = config.get(name);
                } else {
                    if (config.has("*")) {
                        configElement = config.get("*");
                    }
                }
            }

            if (configElement == null)
                return DEFAULT_CONFIG;

            try {
                return (new Gson()).fromJson(configElement, MergeConfig.class);
            } catch (Exception e) {
                log.error("Error getting merge config", e);
                return DEFAULT_CONFIG;
            }

        }

    }

    protected enum MERGE_POSITION {
        BEFORE("before"),
        AFTER("after"),
        INSIDE_FIRST("inside_first"),
        INSIDE_LAST("inside_last");

        public String label;

        private MERGE_POSITION(String label) {
            this.label = label;
        }

        public static MERGE_POSITION valueOfLabel(String label) {
            for (MERGE_POSITION e : values()) {
                if (e.label.equals(label)) {
                    return e;
                }
            }
            return null;
        }

    }

    protected static class MergeConfig {

        public static final String CONFLICT_INGORE = "ignore";
        public static final String CONFLICT_REPLACE = "replace";
        public static final String CONFLICT_MERGE = "merge";

        String targetNodeName;
        String targetNodeNamespace;
        String targetAttributeName;

        String position = MERGE_POSITION.INSIDE_FIRST.label;

        boolean ignoreNamespace = false;

        boolean ignore = false;

        Boolean recoursive = null;

        boolean isTransparent = false;

        boolean createTargetNode = false;
        String targetNodeXML;

        List<String> unique = new ArrayList<>();
        String conflict;

        List<String> conflicts = new ArrayList<>();

        List<String> childrenOrder = new ArrayList<>();

        public boolean isRecoursiveSet() {
            return recoursive != null;
        }

        public boolean isRecoursive() {
            return isRecoursiveSet() ? recoursive : false;
        }

        public void setRecoursive(boolean recoursive) {
            this.recoursive = recoursive;
        }

        public String getTargetNodeName() {
            return targetNodeName;
        }

        public void setTargetNodeName(String targetNodeName) {
            this.targetNodeName = targetNodeName;
        }

        public boolean isCreateTargetNode() {
            return createTargetNode;
        }

        public void setCreateTargetNode(boolean createTargetNode) {
            this.createTargetNode = createTargetNode;
        }

        public String getTargetNodeXML() {
            return targetNodeXML;
        }

        public void setTargetNodXMLe(String targetNodeXML) {
            this.targetNodeXML = targetNodeXML;
        }

        public String getTargetNodeNamespace() {
            return targetNodeNamespace;
        }

        public void setTargetNodeNamespace(String targetNodeNamespace) {
            this.targetNodeNamespace = targetNodeNamespace;
        }

        public MERGE_POSITION getPosition() {
            return MERGE_POSITION.valueOfLabel(position);
        }

        public void setPosition(String position) {
            this.position = position;
        }

        public boolean isIgnoreNamespace() {
            return ignoreNamespace;
        }

        public boolean isIgnore() {
            return ignore;
        }

        public void setIgnore(boolean ignore) {
            this.ignore = ignore;
        }

        public boolean isTransparent() {
            return isTransparent;
        }

        public void setTransparent(boolean isTransparent) {
            this.isTransparent = isTransparent;
        }

        public List<String> getUnique() {
            return unique;
        }

        public void setUnique(List<String> unique) {
            this.unique = unique;
        }

        public String getConflict() {
            return conflict;
        }

        public void setConflict(String conflict) {
            this.conflict = conflict;
        }

        public List<String> getConflicts() {
            return conflicts;
        }

        public void setConflicts(List<String> conflicts) {
            this.conflicts = conflicts;
        }

        public List<String> getChildrenOrder() {
            return childrenOrder;
        }

        public void setChildrenOrder(List<String> childrenOrder) {
            this.childrenOrder = childrenOrder;
        }

    }

}
