/* BdfServer - Copyright (c) 2020-2023 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.tools.uml;

import fr.exemole.bdfserver.api.BdfServer;
import fr.exemole.bdfserver.api.managers.UiManager;
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.api.ui.SubsetIncludeUi;
import fr.exemole.bdfserver.api.ui.UiComponent;
import fr.exemole.bdfserver.api.ui.UiComponents;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import net.fichotheque.Fichotheque;
import net.fichotheque.Subset;
import net.fichotheque.SubsetKey;
import net.fichotheque.corpus.Corpus;
import net.fichotheque.include.ExtendedIncludeKey;
import net.fichotheque.include.IncludeKey;
import net.fichotheque.thesaurus.Thesaurus;
import net.fichotheque.utils.FichothequeMetadataUtils;
import net.mapeadores.util.exceptions.ShouldNotOccurException;
import net.mapeadores.util.localisation.Lang;
import net.mapeadores.util.localisation.MessageLocalisation;
import net.mapeadores.util.uml.PumlUtils;
import net.mapeadores.util.uml.UmlClass;


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

    private final static String THESAURUS_CATEGORY = "thesaurus";
    private final static String CORPUS_CATEGORY = "corpus";
    private final static String SATELLITE_THESAURUS_CATEGORY = "satellite_thesaurus";
    private final static String SATELLITE_CORPUS_CATEGORY = "satellite_corpus";
    private final static String ADDENDA_CATEGORY = "addenda";
    private final static String ALBUM_CATEGORY = "album";
    private final BdfServer bdfServer;
    private final Lang lang;
    private final MessageLocalisation messageLocalisation;
    private final SortedMap<SubsetKey, SubsetInfo> subsetInfoMap = new TreeMap<SubsetKey, SubsetInfo>();
    private final List<SubsetInfo> thesaurusList = new ArrayList<SubsetInfo>();
    private final List<SubsetInfo> corpusList = new ArrayList<SubsetInfo>();
    private final List<SubsetInfo> corpusSatelliteList = new ArrayList<SubsetInfo>();
    private final List<SubsetInfo> thesaurusSatelliteList = new ArrayList<SubsetInfo>();
    private final List<SubsetInfo> addendaList = new ArrayList<SubsetInfo>();
    private final List<SubsetInfo> albumList = new ArrayList<SubsetInfo>();


    public FichothequePumlWriter(BdfServer bdfServer, Lang lang, MessageLocalisation messageLocalisation) {
        this.bdfServer = bdfServer;
        this.lang = lang;
        this.messageLocalisation = messageLocalisation;
    }

    public void write(Appendable appendable) throws IOException {
        scanTree(SubsetKey.CATEGORY_THESAURUS);
        scanTree(SubsetKey.CATEGORY_CORPUS);
        scanTree(SubsetKey.CATEGORY_ADDENDA);
        scanTree(SubsetKey.CATEGORY_ALBUM);
        scanCorpusRelations();
        scanSatelliteRelations(SATELLITE_THESAURUS_CATEGORY);
        scanSatelliteRelations(SATELLITE_CORPUS_CATEGORY);
        Fichotheque fichotheque = bdfServer.getFichotheque();
        PumlUtils.start(appendable);
        PumlUtils.title(appendable, FichothequeMetadataUtils.getTitle(fichotheque, lang));
        PumlUtils.hideEmpties(appendable);
        PumlUtils.orthoLineType(appendable);
        PumlWriter writer = new PumlWriter(appendable);
        writer.write();
        PumlUtils.end(appendable);
    }

    private void scanCorpusRelations() {
        UiManager uiManager = bdfServer.getUiManager();
        for (SubsetInfo corpusInfo : corpusList) {
            UiComponents uiComponents = uiManager.getMainUiComponents((Corpus) corpusInfo.getSubset());
            for (UiComponent uiComponent : uiComponents.getUiComponentList()) {
                if (uiComponent instanceof SubsetIncludeUi) {
                    ExtendedIncludeKey eik = ((SubsetIncludeUi) uiComponent).getExtendedIncludeKey();
                    if (!eik.isMaster()) {
                        SubsetInfo otherInfo = subsetInfoMap.get(eik.getSubsetKey());
                        if (otherInfo != null) {
                            addRelation(corpusInfo, otherInfo, eik.getMode());
                        }
                    }
                }
            }
        }
    }

    private void scanSatelliteRelations(String category) {
        UiManager uiManager = bdfServer.getUiManager();
        for (SubsetInfo corpusInfo : getList(category)) {
            SubsetInfo masterSubsetInfo = subsetInfoMap.get(corpusInfo.getMasterSubsetKey());
            UiComponents uiComponents = uiManager.getMainUiComponents((Corpus) corpusInfo.getSubset());
            for (UiComponent uiComponent : uiComponents.getUiComponentList()) {
                if (uiComponent instanceof SubsetIncludeUi) {
                    ExtendedIncludeKey eik = ((SubsetIncludeUi) uiComponent).getExtendedIncludeKey();
                    SubsetInfo otherInfo = subsetInfoMap.get(eik.getSubsetKey());
                    if (otherInfo != null) {
                        if (eik.isMaster()) {
                            if (masterSubsetInfo.getSubsetKey().isCorpusSubset()) {
                                addRelation(masterSubsetInfo, otherInfo, eik.getMode());
                            } else if (otherInfo.getSubsetKey().isCorpusSubset()) {
                                addRelation(otherInfo, masterSubsetInfo, eik.getMode());
                            }
                        } else {
                            addRelation(corpusInfo, otherInfo, eik.getMode());
                        }
                    }
                }
            }
        }
    }

    private void addRelation(SubsetInfo corpusInfo, SubsetInfo otherInfo, String mode) {
        if (isBefore(corpusInfo, otherInfo)) {
            corpusInfo.addRelation(otherInfo, mode);
        } else {
            otherInfo.addRelation(corpusInfo, mode);
        }
    }

    private void scanTree(short category) {
        SubsetTree subsetTree = bdfServer.getTreeManager().getSubsetTree(category);
        for (SubsetTree.Node node : subsetTree.getNodeList()) {
            if (node instanceof SubsetNode) {
                addSubset(((SubsetNode) node).getSubsetKey());
            } else if (node instanceof GroupNode) {
                scanGroup((GroupNode) node);
            }
        }

    }

    private void scanGroup(GroupNode groupNode) {
        for (SubsetTree.Node node : groupNode.getSubnodeList()) {
            if (node instanceof SubsetNode) {
                addSubset(((SubsetNode) node).getSubsetKey());
            } else if (node instanceof GroupNode) {
                scanGroup((GroupNode) node);
            }
        }
    }

    private void addSubset(SubsetKey subsetKey) {
        Subset subset = bdfServer.getFichotheque().getSubset(subsetKey);
        if (subset == null) {
            return;
        }
        UmlClass umlClass;
        if (subset instanceof Corpus) {
            umlClass = BdfPumlUtils.buildCorpusUmlClass((Corpus) subset, lang, messageLocalisation);
        } else {
            umlClass = BdfPumlUtils.buildUmlClass(subset, lang);
        }
        List<SubsetInfo> list = getList(getCategory(subset));
        SubsetInfo subsetInfo = new SubsetInfo(subset, umlClass, list.size());
        subsetInfoMap.put(subsetKey, subsetInfo);
        list.add(subsetInfo);
    }

    private List<SubsetInfo> getList(String subsetCategory) {
        switch (subsetCategory) {
            case THESAURUS_CATEGORY:
                return thesaurusList;
            case ADDENDA_CATEGORY:
                return addendaList;
            case ALBUM_CATEGORY:
                return albumList;
            case CORPUS_CATEGORY:
                return corpusList;
            case SATELLITE_CORPUS_CATEGORY:
                return corpusSatelliteList;
            case SATELLITE_THESAURUS_CATEGORY:
                return thesaurusSatelliteList;
            default:
                throw new ShouldNotOccurException();
        }
    }


    public static String toString(BdfServer bdfServer, Lang lang, MessageLocalisation messageLocalisation) {
        FichothequePumlWriter fichothequePumlWriter = new FichothequePumlWriter(bdfServer, lang, messageLocalisation);
        try {
            StringBuilder buf = new StringBuilder();
            fichothequePumlWriter.write(buf);
            return buf.toString();
        } catch (IOException ioe) {
            throw new ShouldNotOccurException();
        }
    }


    private class PumlWriter {

        private final Appendable appendable;

        private PumlWriter(Appendable appendable) {
            this.appendable = appendable;
        }

        private void write() throws IOException {
            writeClasses(THESAURUS_CATEGORY);
            writeClasses(SATELLITE_THESAURUS_CATEGORY);
            writeClasses(CORPUS_CATEGORY);
            writeClasses(SATELLITE_CORPUS_CATEGORY);
            writeClasses(ADDENDA_CATEGORY);
            writeClasses(ALBUM_CATEGORY);
            writeMaster(SATELLITE_THESAURUS_CATEGORY);
            writeRelations(THESAURUS_CATEGORY, ">", true);
            writeCorpusRelations(SATELLITE_THESAURUS_CATEGORY);
            writeCorpusRelations(CORPUS_CATEGORY);
            writeMaster(SATELLITE_CORPUS_CATEGORY);
            writeCorpusRelations(SATELLITE_CORPUS_CATEGORY);
            writeRelations(ADDENDA_CATEGORY, "o", false);
            writeRelations(ALBUM_CATEGORY, "o", false);
            writeHiddenPosition(CORPUS_CATEGORY, SATELLITE_THESAURUS_CATEGORY, THESAURUS_CATEGORY);
            writeHiddenPosition(ADDENDA_CATEGORY, SATELLITE_CORPUS_CATEGORY, CORPUS_CATEGORY, SATELLITE_THESAURUS_CATEGORY);
            writeHiddenPosition(ALBUM_CATEGORY, ADDENDA_CATEGORY, SATELLITE_CORPUS_CATEGORY, CORPUS_CATEGORY, SATELLITE_THESAURUS_CATEGORY);
        }

        private void writeClasses(String category) throws IOException {
            List<SubsetInfo> list = getList(category);
            if (list.isEmpty()) {
                return;
            }
            for (SubsetInfo subsetInfo : list) {
                subsetInfo.umlClass.write(appendable, 0);
            }
            appendable.append("\n");
        }

        private void writeMaster(String category) throws IOException {
            for (SubsetInfo corpusInfo : getList(category)) {
                SubsetInfo masterSubsetInfo = subsetInfoMap.get(corpusInfo.getMasterSubsetKey());
                appendable.append(masterSubsetInfo.getName());
                appendable.append(" *-- ");
                appendable.append(corpusInfo.getName());
                appendable.append("\n");
                masterSubsetInfo.addLinkedCategory(category);
                corpusInfo.addLinkedCategory(masterSubsetInfo.subsetCategory);
            }
        }

        private void writeRelations(String category, String relationType, boolean top) throws IOException {
            for (SubsetInfo subsetInfo : getList(category)) {
                String currentName = subsetInfo.getName();
                for (IncludeKey includeKey : subsetInfo.getRelationSet()) {
                    if (top) {
                        appendable.append(currentName);
                        appendable.append(" --");
                        appendable.append(relationType);
                        appendable.append(" ");
                        appendable.append(includeKey.getSubsetKey().getKeyString());

                    } else {
                        appendable.append(includeKey.getSubsetKey().getKeyString());
                        appendable.append(" ");
                        appendable.append(relationType);
                        appendable.append("-- ");
                        appendable.append(currentName);
                    }
                    String mode = includeKey.getMode();
                    if (!mode.isEmpty()) {
                        appendable.append(" : ");
                        appendable.append(mode);
                    }
                    appendable.append("\n");
                }
            }
        }

        private void writeCorpusRelations(String category) throws IOException {
            for (SubsetInfo corpusInfo : getList(category)) {
                String currentName = corpusInfo.getName();
                for (IncludeKey includeKey : corpusInfo.getRelationSet()) {
                    SubsetInfo otherCorpusInfo = subsetInfoMap.get(includeKey.getSubsetKey());
                    String direction;
                    if (corpusInfo.subsetCategory.equals(otherCorpusInfo.subsetCategory)) {
                        direction = "right";
                    } else {
                        direction = "down";
                    }
                    appendable.append(currentName);
                    appendable.append(" -");
                    appendable.append(direction);
                    appendable.append("- ");
                    appendable.append(includeKey.getSubsetKey().getKeyString());
                    appendable.append("\n");
                }
            }
        }

        private void writeHiddenPosition(String category, String... upCategories) throws IOException {
            List<SubsetInfo> downList = getList(category);
            if (downList.isEmpty()) {
                return;
            }
            boolean done = false;
            for (String upCategory : upCategories) {
                List<SubsetInfo> upList = getList(upCategory);
                if (!upList.isEmpty()) {
                    done = true;
                    writeHiddenLinks(upCategory, upList, downList);
                    break;
                }
            }
        }

        private void writeHiddenLinks(String upCategory, List<SubsetInfo> upList, List<SubsetInfo> downList) throws IOException {
            int upSize = upList.size();
            int downSize = downList.size();
            if (upSize == downSize) {
                for (int i = 0; i < upSize; i++) {
                    SubsetInfo upInfo = upList.get(i);
                    SubsetInfo downInfo = downList.get(i);
                    if (!downInfo.containsLinkedCategory(upCategory)) {
                        writeHidden(upInfo, downInfo);
                    }
                }
            } else if (upSize > downSize) {
                int diff = ((upSize - downSize) / 2);
                for (int i = 0; i < downSize; i++) {
                    SubsetInfo upInfo = upList.get(i + diff);
                    SubsetInfo downInfo = downList.get(i);
                    if (!downInfo.containsLinkedCategory(upCategory)) {
                        writeHidden(upInfo, downInfo);
                    }
                }
            } else {
                int upIndex = 0;
                int count = 0;
                int quotient = downSize / upSize;
                int reste = downSize % upSize;
                for (int i = 0; i < downSize; i++) {
                    SubsetInfo upInfo = upList.get(upIndex);
                    SubsetInfo downInfo = downList.get(i);
                    if (!downInfo.containsLinkedCategory(upCategory)) {
                        writeHidden(upInfo, downInfo);
                    }
                    count++;
                    if (count == quotient) {
                        if (reste > 0) {
                            reste--;
                            count--;
                        } else {
                            upIndex++;
                            count = 0;
                        }
                    }
                }
            }
        }

        private void writeHidden(SubsetInfo upInfo, SubsetInfo downInfo) throws IOException {
            appendable.append(upInfo.getName());
            appendable.append(" -[hidden]- ");
            appendable.append(downInfo.getName());
            appendable.append("\n");
        }

    }


    private static class SubsetInfo {

        private final Subset subset;
        private final UmlClass umlClass;
        private final String subsetCategory;
        protected final int index;
        private final Set<IncludeKey> relationSet = new LinkedHashSet<IncludeKey>();
        private final Set<String> linkedSategorySet = new HashSet<String>();

        protected SubsetInfo(Subset subset, UmlClass umlClass, int index) {
            this.subset = subset;
            this.umlClass = umlClass;
            this.index = index;
            this.subsetCategory = getCategory(subset);
        }

        public String getName() {
            return subset.getSubsetKeyString();
        }

        public Subset getSubset() {
            return subset;
        }

        public SubsetKey getSubsetKey() {
            return subset.getSubsetKey();
        }

        public SubsetKey getMasterSubsetKey() {
            switch (subsetCategory) {
                case SATELLITE_THESAURUS_CATEGORY:
                case SATELLITE_CORPUS_CATEGORY:
                    return ((Corpus) subset).getMasterSubset().getSubsetKey();
                default:
                    return null;
            }
        }

        public String getSubsetCategory() {
            return subsetCategory;
        }


        public boolean isEmpty() {
            return relationSet.isEmpty();
        }

        public void addIncludeKey(IncludeKey includeKey) {
            relationSet.add(includeKey);
        }

        public void addRelation(SubsetInfo subsetInfo, String mode) {
            relationSet.add(IncludeKey.newInstance(subsetInfo.getSubsetKey(), mode, -1));
            linkedSategorySet.add(subsetInfo.subsetCategory);
            subsetInfo.linkedSategorySet.add(this.subsetCategory);
        }

        public void addLinkedCategory(String category) {
            linkedSategorySet.add(category);
        }

        public boolean containsLinkedCategory(String category) {
            return linkedSategorySet.contains(category);
        }


        public Set<IncludeKey> getRelationSet() {
            return relationSet;
        }

    }

    private static boolean isBefore(SubsetInfo corpusInfo, SubsetInfo otherInfo) {
        if (corpusInfo.subsetCategory.equals(otherInfo.subsetCategory)) {
            return (corpusInfo.index < otherInfo.index);
        }
        switch (corpusInfo.subsetCategory) {
            case SATELLITE_THESAURUS_CATEGORY: {
                switch (otherInfo.subsetCategory) {
                    case CORPUS_CATEGORY:
                    case SATELLITE_CORPUS_CATEGORY:
                        return true;
                    default:
                        return false;
                }
            }
            case CORPUS_CATEGORY: {
                switch (otherInfo.subsetCategory) {
                    case SATELLITE_CORPUS_CATEGORY:
                        return true;
                    default:
                        return false;
                }
            }
            default:
                return false;
        }
    }

    private static String getCategory(Subset subset) {
        switch (subset.getSubsetKey().getCategory()) {
            case SubsetKey.CATEGORY_THESAURUS:
                return THESAURUS_CATEGORY;
            case SubsetKey.CATEGORY_ADDENDA:
                return ADDENDA_CATEGORY;
            case SubsetKey.CATEGORY_ALBUM:
                return ALBUM_CATEGORY;
            case SubsetKey.CATEGORY_CORPUS:
                Subset masterSubset = ((Corpus) subset).getMasterSubset();
                if (masterSubset == null) {
                    return CORPUS_CATEGORY;
                } else if (masterSubset instanceof Thesaurus) {
                    return SATELLITE_THESAURUS_CATEGORY;
                } else {
                    return SATELLITE_CORPUS_CATEGORY;
                }
            default:
                throw new ShouldNotOccurException();
        }
    }

}
