/* BdfServer - Copyright (c) 2014-2020 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 fr.exemole.bdfserver.impl;

import fr.exemole.bdfserver.api.managers.TreeManager;
import fr.exemole.bdfserver.api.storage.TreeStorage;
import fr.exemole.bdfserver.api.subsettree.GroupNode;
import fr.exemole.bdfserver.api.subsettree.SubsetNode;
import fr.exemole.bdfserver.api.subsettree.SubsetTree;
import fr.exemole.bdfserver.tools.subsettree.SubsetNodeBuilder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import net.fichotheque.Fichotheque;
import net.fichotheque.FichothequeEditor;
import net.fichotheque.FichothequeListener;
import net.fichotheque.Subset;
import net.fichotheque.SubsetItem;
import net.fichotheque.SubsetKey;
import net.fichotheque.addenda.Addenda;
import net.fichotheque.album.Album;
import net.fichotheque.corpus.Corpus;
import net.fichotheque.corpus.FicheMeta;
import net.fichotheque.corpus.fiche.FicheAPI;
import net.fichotheque.corpus.metadata.CorpusField;
import net.fichotheque.corpus.metadata.FieldKey;
import net.fichotheque.sphere.Sphere;
import net.fichotheque.thesaurus.Thesaurus;


/**
 *
 * @author Vincent Calame
 */
class TreeManagerImpl implements TreeManager, FichothequeListener {

    private final Fichotheque fichotheque;
    private final TreeStorage treeStorage;
    private final Map<Short, InternalSubsetTree> subsetTreeMap = new HashMap<Short, InternalSubsetTree>();

    private TreeManagerImpl(Fichotheque fichotheque, TreeStorage treeStorage) {
        this.fichotheque = fichotheque;
        this.treeStorage = treeStorage;
    }

    static TreeManagerImpl build(Fichotheque fichotheque, TreeStorage treeStorage, Map<Short, SubsetTree> treeMap) {
        TreeManagerImpl impl = new TreeManagerImpl(fichotheque, treeStorage);
        impl.updateSubsetTree(SubsetKey.CATEGORY_CORPUS, treeMap);
        impl.updateSubsetTree(SubsetKey.CATEGORY_THESAURUS, treeMap);
        impl.updateSubsetTree(SubsetKey.CATEGORY_SPHERE, treeMap);
        impl.updateSubsetTree(SubsetKey.CATEGORY_ALBUM, treeMap);
        impl.updateSubsetTree(SubsetKey.CATEGORY_ADDENDA, treeMap);
        return impl;
    }

    @Override
    public SubsetTree getSubsetTree(short subsetCategory) {
        return subsetTreeMap.get(subsetCategory);
    }

    @Override
    public void subsetCreated(FichothequeEditor fichothequeEditor, Subset subset) {
        SubsetKey subsetKey = subset.getSubsetKey();
        short subsetCategory = subsetKey.getCategory();
        InternalSubsetTree subsetTree = subsetTreeMap.get(subsetCategory);
        subsetTree.addNode(SubsetNodeBuilder.build(subsetKey));
        treeStorage.saveSubsetTree(subsetCategory, subsetTree);
    }

    @Override
    public void subsetRemoved(FichothequeEditor fichothequeEditor, SubsetKey subsetKey, Subset masterSubset) {
        short subsetCategory = subsetKey.getCategory();
        InternalSubsetTree subsetTree = subsetTreeMap.get(subsetCategory);
        boolean done = subsetTree.removeSubset(subsetKey);
        if (done) {
            treeStorage.saveSubsetTree(subsetCategory, subsetTree);
        }
    }

    @Override
    public void corpusFieldCreated(FichothequeEditor fichothequeEditor, Corpus corpus, CorpusField corpusField) {
    }

    @Override
    public void corpusFieldRemoved(FichothequeEditor fichothequeEditor, Corpus corpus, FieldKey fieldKey) {
    }

    @Override
    public void subsetItemCreated(FichothequeEditor fichothequeEditor, SubsetItem subsetItem) {
    }

    @Override
    public void ficheSaved(FichothequeEditor fichothequeEditor, FicheMeta ficheMeta, FicheAPI fiche) {
    }

    @Override
    public void subsetItemRemoved(FichothequeEditor fichothequeEditor, Subset subset, int id) {
    }

    private void updateSubsetTree(short subsetCategory, Map<Short, SubsetTree> treeMap) {
        updateSubsetTree(subsetCategory, treeMap.get(subsetCategory));
    }

    void updateSubsetTree(short subsetCategory, SubsetTree originalSubsetTree) {
        SortedSet<SubsetKey> existingSubsetSet = getExistingSubsetSet(subsetCategory, fichotheque);
        TreeChecker treeChecker = new TreeChecker(subsetCategory, existingSubsetSet);
        InternalSubsetTree internalSubsetTree = subsetTreeMap.get(subsetCategory);
        if (internalSubsetTree == null) {
            internalSubsetTree = new InternalSubsetTree(subsetCategory);
            subsetTreeMap.put(subsetCategory, internalSubsetTree);
        } else {
            internalSubsetTree.clear();
        }
        if (originalSubsetTree != null) {
            for (SubsetTree.Node node : originalSubsetTree.getNodeList()) {
                if (node instanceof SubsetNode) {
                    SubsetKey subsetKey = ((SubsetNode) node).getSubsetKey();
                    if (treeChecker.check(subsetKey)) {
                        internalSubsetTree.addNode(SubsetNodeBuilder.build(subsetKey));
                    }
                } else if (node instanceof GroupNode) {
                    InternalGroupNode groupNode = buildGroupNode((GroupNode) node, treeChecker, internalSubsetTree);
                    if (groupNode != null) {
                        internalSubsetTree.addNode(groupNode);
                    }
                }
            }
        }
        treeChecker.endCheck(internalSubsetTree);
    }

    void saveSubsetTree(short subsetCategory) {
        InternalSubsetTree subsetTree = subsetTreeMap.get(subsetCategory);
        if (subsetTree != null) {
            treeStorage.saveSubsetTree(subsetCategory, subsetTree);
        }
    }

    private InternalGroupNode buildGroupNode(GroupNode groupNode, TreeChecker treeChecker, NodeConsumer currentConsumer) {
        String groupName = groupNode.getName();
        InternalGroupNode ign = null;
        if (treeChecker.check(groupName)) {
            ign = new InternalGroupNode(groupName);
            currentConsumer = ign;
        }
        for (SubsetTree.Node subnode : groupNode.getSubnodeList()) {
            if (subnode instanceof SubsetNode) {
                SubsetKey subsetKey = ((SubsetNode) subnode).getSubsetKey();
                if (treeChecker.check(subsetKey)) {
                    currentConsumer.addNode(SubsetNodeBuilder.build(subsetKey));
                }
            } else if (subnode instanceof GroupNode) {
                InternalGroupNode child = buildGroupNode((GroupNode) subnode, treeChecker, currentConsumer);
                if (child != null) {
                    currentConsumer.addNode(child);
                }
            }
        }
        return ign;
    }


    private static abstract class NodeConsumer {

        abstract void addNode(SubsetTree.Node node);

    }


    private static class InternalSubsetTree extends NodeConsumer implements SubsetTree {

        private final List<SubsetTree.Node> nodeList = new ArrayList<SubsetTree.Node>();
        private final List<SubsetTree.Node> unmodifiableNodeList = Collections.unmodifiableList(nodeList);
        private final short subsetCategory;

        private InternalSubsetTree(short subsetCategory) {
            this.subsetCategory = subsetCategory;
        }

        @Override
        public List<SubsetTree.Node> getNodeList() {
            return unmodifiableNodeList;
        }

        @Override
        void addNode(SubsetTree.Node node) {
            nodeList.add(node);
        }

        private void clear() {
            nodeList.clear();
        }

        private boolean removeSubset(SubsetKey subsetKey) {
            int length = nodeList.size();
            for (int i = 0; i < length; i++) {
                SubsetTree.Node node = nodeList.get(i);
                if (node instanceof SubsetNode) {
                    if (((SubsetNode) node).getSubsetKey().equals(subsetKey)) {
                        nodeList.remove(i);
                        return true;
                    }
                } else if (node instanceof GroupNode) {
                    boolean done = ((InternalGroupNode) node).removeSubset(subsetKey);
                    if (done) {
                        return true;
                    }
                }
            }
            return false;
        }

    }


    private static class InternalGroupNode extends NodeConsumer implements GroupNode {

        private final List<SubsetTree.Node> subnodeList = new ArrayList<SubsetTree.Node>();
        private final List<SubsetTree.Node> unmodifiableSubnodeList = Collections.unmodifiableList(subnodeList);
        private final String name;

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

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

        @Override
        public List<SubsetTree.Node> getSubnodeList() {
            return unmodifiableSubnodeList;
        }

        @Override
        void addNode(SubsetTree.Node node) {
            subnodeList.add(node);
        }

        private void clear() {
            subnodeList.clear();
        }

        private boolean removeSubset(SubsetKey subsetKey) {
            int length = subnodeList.size();
            for (int i = 0; i < length; i++) {
                SubsetTree.Node subnode = subnodeList.get(i);
                if (subnode instanceof SubsetNode) {
                    if (((SubsetNode) subnode).getSubsetKey().equals(subsetKey)) {
                        subnodeList.remove(i);
                        return true;
                    }
                } else if (subnode instanceof GroupNode) {
                    boolean done = ((InternalGroupNode) subnode).removeSubset(subsetKey);
                    if (done) {
                        return true;
                    }
                }
            }
            return false;
        }

    }


    private static class TreeChecker {

        private final short subsetCategory;
        private final SortedSet<SubsetKey> existingSubsetSet;
        private final Set<String> groupNameSet = new HashSet<String>();

        private TreeChecker(short subsetCategory, SortedSet<SubsetKey> existingSubsetSet) {
            this.subsetCategory = subsetCategory;
            this.existingSubsetSet = existingSubsetSet;

        }

        private boolean check(String groupName) {
            if (groupNameSet.contains(groupName)) {
                return false;
            } else {
                groupNameSet.add(groupName);
                return true;
            }
        }

        private boolean check(SubsetKey subsetKey) {
            if (subsetKey.getCategory() != subsetCategory) {
                return false;
            }
            if (!existingSubsetSet.contains(subsetKey)) {
                return false;
            }
            existingSubsetSet.remove(subsetKey);
            return true;
        }

        private void endCheck(InternalSubsetTree subsetTree) {
            for (SubsetKey subsetKey : existingSubsetSet) {
                subsetTree.addNode(SubsetNodeBuilder.build(subsetKey));
            }
        }

    }

    private static SortedSet<SubsetKey> getExistingSubsetSet(short subsetCategory, Fichotheque fichotheque) {
        SortedSet<SubsetKey> existingSubsetSet = new TreeSet<SubsetKey>();
        if (subsetCategory == SubsetKey.CATEGORY_CORPUS) {
            for (Corpus corpus : fichotheque.getCorpusList()) {
                existingSubsetSet.add(corpus.getSubsetKey());
            }
        } else if (subsetCategory == SubsetKey.CATEGORY_THESAURUS) {
            for (Thesaurus thesaurus : fichotheque.getThesaurusList()) {
                existingSubsetSet.add(thesaurus.getSubsetKey());
            }
        } else if (subsetCategory == SubsetKey.CATEGORY_SPHERE) {
            for (Sphere sphere : fichotheque.getSphereList()) {
                existingSubsetSet.add(sphere.getSubsetKey());
            }
        } else if (subsetCategory == SubsetKey.CATEGORY_ALBUM) {
            for (Album album : fichotheque.getAlbumList()) {
                existingSubsetSet.add(album.getSubsetKey());
            }
        } else if (subsetCategory == SubsetKey.CATEGORY_ADDENDA) {
            for (Addenda addenda : fichotheque.getAddendaList()) {
                existingSubsetSet.add(addenda.getSubsetKey());
            }
        }
        return existingSubsetSet;
    }

}
