/* FichothequeLib_Tools - Copyright (c) 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.fichotheque.tools.exportation.balayage.dom;

import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import net.mapeadores.util.logging.MessageHandler;
import net.mapeadores.util.xml.DOMUtils;
import net.mapeadores.util.xml.DocumentFragmentHolderBuilder;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;


/**
 *
 * @author Vincent Calame
 */
public class IncludeCatalogDOMReader {

    private final static short RESOLVED_ALL = 1;
    private final static short RESOLVED_SOME = 2;
    private final static short RESOLVED_NONE = 3;

    private final DocumentFragmentHolderBuilder documentFragmentHolderBuilder;
    private final MessageHandler messageHandler;

    public IncludeCatalogDOMReader(DocumentFragmentHolderBuilder documentFragmentHolderBuilder, MessageHandler messageHandler) {
        this.documentFragmentHolderBuilder = documentFragmentHolderBuilder;
        this.messageHandler = messageHandler;
    }

    public IncludeCatalogDOMReader read(Element element) {
        CatalogBuffer catalogBuffer = new CatalogBuffer();
        DOMUtils.readChildren(element, new RootConsumer(catalogBuffer));
        catalogBuffer.flush(documentFragmentHolderBuilder);
        return this;
    }

    public static IncludeCatalogDOMReader init(DocumentFragmentHolderBuilder documentFragmentHolderBuilder, MessageHandler messageHandler) {
        return new IncludeCatalogDOMReader(documentFragmentHolderBuilder, messageHandler);
    }


    private class RootConsumer implements Consumer<Element> {

        private final CatalogBuffer catalogBuffer;

        private RootConsumer(CatalogBuffer fragmentBuffer) {
            this.catalogBuffer = fragmentBuffer;
        }

        @Override
        public void accept(Element element) {
            String tagName = element.getTagName();
            switch (tagName) {
                case "fragment":
                    String key = element.getAttribute("key");
                    catalogBuffer.addIncludeFragment(key, element.getChildNodes());
                    break;
            }
        }

    }


    private static class CatalogBuffer {

        private final Document document = DOMUtils.newDocument();
        private final Map<String, DocumentFragment> finalFragmentMap = new HashMap<String, DocumentFragment>();
        private final Map<String, FragmentBuffer> unresolvedFragmentMap = new HashMap<String, FragmentBuffer>();

        private CatalogBuffer() {
        }

        private void flush(DocumentFragmentHolderBuilder documentFragmentHolderBuilder) {
            resolve();
            for (Map.Entry<String, DocumentFragment> entry : finalFragmentMap.entrySet()) {
                documentFragmentHolderBuilder.put(entry.getKey(), entry.getValue());
            }
        }

        private void resolve() {
            if (unresolvedFragmentMap.isEmpty()) {
                return;
            }
            boolean oneDone = false;
            for (Iterator<FragmentBuffer> it = unresolvedFragmentMap.values().iterator(); it.hasNext();) {
                FragmentBuffer documentFragmentBuilder = it.next();
                short result = documentFragmentBuilder.resolve(finalFragmentMap);
                switch (result) {
                    case RESOLVED_ALL:
                        it.remove();
                        oneDone = true;
                        break;
                    case RESOLVED_SOME:
                        oneDone = true;
                        break;
                    case RESOLVED_NONE:

                }
            }
            if (oneDone) {
                if (unresolvedFragmentMap.isEmpty()) {
                    return;
                } else {
                    resolve();
                }
            } else {
                //@todo avertissement toutes les inclusions n'ont pas été résolues
                return;
            }
        }

        private void addIncludeFragment(String key, NodeList nodeList) {
            if (finalFragmentMap.containsKey(key)) {
                finalFragmentMap.remove(key);
            }
            if (unresolvedFragmentMap.containsKey(key)) {
                unresolvedFragmentMap.remove(key);
            }
            DocumentFragment documentFragment = document.createDocumentFragment();
            int count = nodeList.getLength();
            for (int i = 0; i < count; i++) {
                Node node = nodeList.item(i);
                Node importNode = document.importNode(node, true);
                documentFragment.appendChild(importNode);
            }
            FragmentBuffer fragmentBuffer = new FragmentBuffer(key, documentFragment);
            if (fragmentBuffer.hasIncludeElement()) {
                unresolvedFragmentMap.put(key, fragmentBuffer);
            } else {
                finalFragmentMap.put(key, documentFragment);
            }
        }

    }


    private static class FragmentBuffer {

        private final String key;
        private final DocumentFragment documentFragment;
        private final List<Element> elementList = new LinkedList<Element>();

        private FragmentBuffer(String key, DocumentFragment documentFragment) {
            this.key = key;
            this.documentFragment = documentFragment;
            NodeList list = documentFragment.getChildNodes();
            int count = list.getLength();
            for (int i = 0; i < count; i++) {
                Node node = list.item(i);
                if (node instanceof Element) {
                    Element element = (Element) node;
                    if (element.getTagName().equals("include")) {
                        addIncludeElement(element);
                    } else {
                        NodeList includeChildren = element.getElementsByTagName("include");
                        int includeCount = includeChildren.getLength();
                        for (int j = 0; j < includeCount; j++) {
                            addIncludeElement((Element) includeChildren.item(j));
                        }
                    }
                }
            }
        }

        private short resolve(Map<String, DocumentFragment> finalFragmentMap) {
            boolean oneDone = false;
            for (Iterator<Element> it = elementList.iterator(); it.hasNext();) {
                Element element = it.next();
                String fragmentKey = element.getAttribute("fragment-key");
                DocumentFragment finalFragment = finalFragmentMap.get(fragmentKey);
                if (finalFragment != null) {
                    finalFragment = (DocumentFragment) finalFragment.cloneNode(true);
                    element.getParentNode().replaceChild(finalFragment, element);
                    oneDone = true;
                    it.remove();
                }
            }
            if (oneDone) {
                if (elementList.isEmpty()) {
                    finalFragmentMap.put(key, documentFragment);
                    return RESOLVED_ALL;
                } else {
                    return RESOLVED_SOME;
                }
            } else {
                return RESOLVED_NONE;
            }
        }

        private boolean hasIncludeElement() {
            return (elementList.size() > 0);
        }

        private void addIncludeElement(Element element) {
            NodeList includeChildren = element.getElementsByTagName("include");
            if (includeChildren.getLength() == 0) {
                String fragmentKey = element.getAttribute("fragment-key");
                if (fragmentKey.length() > 0) {
                    elementList.add(element);
                }
            } else {
                //@todo : avertissement, un élément include ne doit pas contenir un autre élément include
            }
        }

    }

}
