/* BdfServer - Copyright (c) 2014-2022 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.memento;

import fr.exemole.bdfserver.api.BdfServer;
import fr.exemole.bdfserver.api.instruction.BdfParameters;
import fr.exemole.bdfserver.api.managers.TransformationManager;
import fr.exemole.bdfserver.api.memento.MementoNode;
import fr.exemole.bdfserver.api.memento.MementoUnit;
import fr.exemole.bdfserver.api.storage.BdfStorageException;
import fr.exemole.bdfserver.api.users.BdfUser;
import fr.exemole.bdfserver.tools.exportation.transformation.TransformerParameters;
import fr.exemole.bdfserver.tools.memento.dom.MementoTreeDOMReader;
import fr.exemole.bdfserver.tools.storage.StorageUtils;
import fr.exemole.bdfserver.tools.users.BdfUserUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.xml.transform.dom.DOMSource;
import net.fichotheque.exportation.transformation.SimpleTemplate;
import net.fichotheque.exportation.transformation.TemplateKey;
import net.fichotheque.exportation.transformation.TransformationKey;
import net.mapeadores.util.exceptions.NestedTransformerException;
import net.mapeadores.util.io.DocStream;
import net.mapeadores.util.io.ResourceFolder;
import net.mapeadores.util.io.ResourceStorage;
import net.mapeadores.util.io.ResourceStorages;
import net.mapeadores.util.localisation.Lang;
import net.mapeadores.util.localisation.LangPreference;
import net.mapeadores.util.text.RelativePath;
import net.mapeadores.util.xml.DOMUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;


/**
 * Moteur qui construit le mémento à partir des ressources statiques de la base.
 * Le mémento complet est construit à partir du contenu de xml/memento : chaque
 * répertoire de xml/memento désigne une unité du mémento.
 *
 * Le moteur recherche également les unités ajoutées par des extensions en
 * vérifiant la présence de xml/memento dans les ressources de l'extension (par
 * exemple : ext-rsc/resourcesupdate/xml/memento)
 *
 *
 * @author Vincent Calame
 */
public class MementoEngine {

    private final static RelativePath CORE_PATH = RelativePath.build("xml/memento");
    private final static Lang ENGLISH = Lang.build("en");
    private final static Lang FRENCH = Lang.build("fr");
    private final BdfParameters bdfParameters;
    private final BdfServer bdfServer;
    private final BdfUser bdfUser;
    private final ResourceStorages resourceStorages;

    private MementoEngine(BdfParameters bdfParameters) {
        this.bdfParameters = bdfParameters;
        this.bdfServer = bdfParameters.getBdfServer();
        this.bdfUser = bdfParameters.getBdfUser();
        this.resourceStorages = bdfServer.getResourceStorages();
    }

    public static List<MementoUnit.Info> getMementoUnitInfoList(BdfParameters bdfParameters) {
        MementoEngine mementoEngine = new MementoEngine(bdfParameters);
        return mementoEngine.scanMementoUnitInfoList();
    }

    public static MementoUnit getMementoUnit(BdfParameters bdfParameters, RelativePath path) {
        MementoEngine mementoEngine = new MementoEngine(bdfParameters);
        return mementoEngine.getMementoUnit(path);
    }

    private MementoUnit getMementoUnit(RelativePath path) {
        Document treeDocument;
        try {
            treeDocument = getUnitTreeDocument(path);
            if (treeDocument == null) {
                return getErrorUnit(path, "Error: missing tree.xml");
            }
        } catch (SAXException saxe) {
            return getErrorUnit(path, "Error: invalid tree.xml / " + saxe.getMessage());
        }
        UnitScan unitScan = new UnitScan(path);
        for (ResourceStorage resourceStorage : resourceStorages) {
            ResourceFolder folder = resourceStorage.getResourceFolder(path);
            if (folder != null) {
                unitScan.scan(folder);
            }
        }
        TemplateKey templateKey = BdfUserUtils.getSimpleTemplateKey(bdfParameters, TransformationKey.MEMENTO_INSTANCE);
        TransformationManager transformationManager = bdfServer.getTransformationManager();
        SimpleTemplate simpleTemplate = transformationManager.getSimpleTemplate(templateKey, true);
        return unitScan.toUnit(bdfUser.getLangPreference(), MementoTreeDOMReader.readNode(treeDocument.getDocumentElement()), simpleTemplate);
    }

    private List<MementoUnit.Info> scanMementoUnitInfoList() {
        LangPreference langPreference = bdfUser.getLangPreference();
        List<MementoUnit.Info> result = new ArrayList<MementoUnit.Info>();
        SortedMap<String, SourceScan> scanMap = new TreeMap<String, SourceScan>();
        for (ResourceStorage resourceStorage : resourceStorages) {
            scanResourceStorage(resourceStorage, scanMap);
        }
        for (SourceScan scan : scanMap.values()) {
            scan.populate(result, langPreference);
        }
        return result;
    }


    private void scanResourceStorage(ResourceStorage resourceStorage, SortedMap<String, SourceScan> scanMap) {
        ResourceFolder coreFolder = resourceStorage.getResourceFolder(CORE_PATH);
        if (coreFolder != null) {
            SourceScan scan = scanMap.get("");
            if (scan == null) {
                scan = new SourceScan("", CORE_PATH);
                scanMap.put("", scan);
            }
            scan.scanUnit(coreFolder);
        }
        ResourceFolder extrscFolder = resourceStorage.getResourceFolder(StorageUtils.EXTENSION_RESOURCE_ROOT);
        if (extrscFolder != null) {
            for (ResourceFolder subfolder : extrscFolder.getSubfolderList()) {
                String name = subfolder.getName();
                RelativePath path = StorageUtils.EXTENSION_RESOURCE_ROOT.buildChild(name + "/xml/memento");
                ResourceFolder extFolder = resourceStorage.getResourceFolder(path);
                if (extFolder != null) {
                    SourceScan scan = scanMap.get(name);
                    if (scan == null) {
                        scan = new SourceScan(name, path);
                        scanMap.put(name, scan);
                    }
                    scan.scanUnit(extFolder);
                }
            }
        }
    }

    private Document getUnitTreeDocument(RelativePath unitRelativePath) throws SAXException {
        DocStream docStream = bdfServer.getResourceStorages().getResourceDocStream(unitRelativePath.buildChild("tree.xml"));
        if (docStream == null) {
            return null;
        }
        try {
            return DOMUtils.parseDocument(docStream);
        } catch (IOException ioe) {
            throw new BdfStorageException(ioe);
        }
    }

    private List<String> getOrderedNameList(RelativePath sourceRelativePath) {
        List<String> result = new ArrayList<String>();
        DocStream docStream = bdfServer.getResourceStorages().getResourceDocStream(sourceRelativePath.buildChild("order.txt"));
        if (docStream != null) {
            try (BufferedReader reader = new BufferedReader(docStream.getReader())) {
                String ligne;
                while ((ligne = reader.readLine()) != null) {
                    ligne = ligne.trim();
                    if ((ligne.length() > 0) && (!ligne.startsWith("#"))) {
                        result.add(ligne);
                    }
                }
            } catch (IOException ioe) {
                throw new BdfStorageException(ioe);
            }
        }
        return result;
    }

    private static MementoUnit getErrorUnit(RelativePath path, String title) {
        return new InternalMementoUnit(path, title, MementoUtils.EMPTY_NODELIST);
    }


    private class SourceScan {

        private final String name;
        private final RelativePath relativePath;
        private final SortedMap<String, UnitScan> unitScanMap = new TreeMap<String, UnitScan>();

        private SourceScan(String name, RelativePath relativePath) {
            this.name = name;
            this.relativePath = relativePath;
        }

        private void scanUnit(ResourceFolder sourceFolder) {
            for (ResourceFolder subfolder : sourceFolder.getSubfolderList()) {
                String name = subfolder.getName();
                UnitScan current = unitScanMap.get(name);
                if (current == null) {
                    current = new UnitScan(relativePath.buildChild(name));
                    unitScanMap.put(name, current);
                }
                current.scan(subfolder);
            }
        }

        private void populate(List<MementoUnit.Info> list, LangPreference langPreference) {
            List<String> orderedNameList = getOrderedNameList(relativePath);
            for (String name : orderedNameList) {
                UnitScan unitScan = unitScanMap.get(name);
                if (unitScan != null) {
                    unitScanMap.remove(name);
                    list.add(unitScan.toInfo(langPreference));
                }
            }
            for (UnitScan unitScan : unitScanMap.values()) {
                list.add(unitScan.toInfo(langPreference));
            }
        }

    }


    private class UnitScan {

        private final String name;
        private final RelativePath unitPath;
        private final Set<Lang> availableLangSet = new HashSet<Lang>();

        private UnitScan(RelativePath unitPath) {
            this.name = unitPath.getLastName();
            this.unitPath = unitPath;
        }

        private void scan(ResourceFolder unitFolder) {
            for (ResourceFolder langFolder : unitFolder.getSubfolderList()) {
                try {
                    Lang lang = Lang.parse(langFolder.getName());
                    availableLangSet.add(lang);
                } catch (ParseException pe) {
                }
            }
        }


        private MementoUnit.Info toInfo(LangPreference langPreference) {
            String title = testTreeFile();
            if (title == null) {
                title = getUnitTitle(langPreference);
            }
            return new InternalInfo(name, unitPath, title);
        }

        private MementoUnit toUnit(LangPreference langPreference, MementoTreeDOMReader.Node rootNode, SimpleTemplate simpleTemplate) {
            String title = getUnitTitle(langPreference);
            List<MementoNode> nodeList = new ArrayList<MementoNode>();
            for (MementoTreeDOMReader.Node subnode : rootNode.getSubnodeList()) {
                nodeList.add(toMementoNode(langPreference, subnode, simpleTemplate));
            }
            return new InternalMementoUnit(unitPath, title, nodeList);
        }

        private MementoNode toMementoNode(LangPreference langPreference, MementoTreeDOMReader.Node elNode, SimpleTemplate simpleTemplate) {
            MementoNodeBuilder builder = new MementoNodeBuilder(elNode.getName());
            for (MementoTreeDOMReader.Node subnode : elNode.getSubnodeList()) {
                builder.addSubnode(toMementoNode(langPreference, subnode, simpleTemplate));
            }
            DocumentInfo documentInfo = getNodeDocumentInfo(langPreference, elNode.getName());
            if (documentInfo != null) {
                builder.setTitle(documentInfo.getTitle());
                builder.setText(documentInfo.transform(simpleTemplate));
            } else {
                builder.setTitle("Missing */" + elNode.getName() + ".xml");
            }
            return builder.toMementoNode();
        }

        private String getUnitTitle(LangPreference langPreference) {
            DocumentInfo documentInfo = getNodeDocumentInfo(langPreference, name);
            if (documentInfo != null) {
                return documentInfo.getTitle();
            } else {
                return name;
            }
        }

        private String testTreeFile() {
            try {
                Document document = getUnitTreeDocument(unitPath);
                if (document == null) {
                    return "Error: missing tree.xml";
                }
            } catch (SAXException sax) {
                return "Error: invalid tree.xml / " + sax.getMessage();
            }
            return null;
        }

        private DocumentInfo getNodeDocumentInfo(LangPreference langPreference, String nodeName) {
            DocumentInfo documentInfo = null;
            for (Lang lang : langPreference) {
                documentInfo = UnitScan.this.getNodeDocumentInfo(lang, nodeName);
                if ((documentInfo == null) && (!lang.isRootLang())) {
                    documentInfo = UnitScan.this.getNodeDocumentInfo(lang.getRootLang(), nodeName);
                }
                if (documentInfo != null) {
                    break;
                }
            }
            if (documentInfo == null) {
                if (availableLangSet.contains(ENGLISH)) {
                    documentInfo = UnitScan.this.getNodeDocumentInfo(ENGLISH, nodeName);
                }
                if ((documentInfo == null) && (availableLangSet.contains(FRENCH))) {
                    documentInfo = UnitScan.this.getNodeDocumentInfo(FRENCH, nodeName);
                }
                if (documentInfo == null) {
                    for (Lang lang : availableLangSet) {
                        documentInfo = UnitScan.this.getNodeDocumentInfo(lang, nodeName);
                        if (documentInfo != null) {
                            break;
                        }
                    }
                }
            }
            return documentInfo;
        }

        private DocumentInfo getNodeDocumentInfo(Lang lang, String nodeName) {
            DocStream docStream = bdfServer.getResourceStorages().getResourceDocStream(unitPath.buildChild(lang.toString() + "/" + nodeName + ".xml"));
            if (docStream == null) {
                return null;
            }
            try {
                Document document = DOMUtils.parseDocument(docStream);
                return new DocumentInfo(lang, name, document);
            } catch (IOException | SAXException e) {
                return new DocumentInfo(lang, name, e.getMessage());
            }
        }

    }


    private static class InternalInfo implements MementoUnit.Info {

        private final String name;
        private final RelativePath path;
        private final String title;

        private InternalInfo(String name, RelativePath path, String title) {
            this.name = name;
            this.path = path;
            this.title = title;
        }

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

        @Override
        public RelativePath getPath() {
            return path;
        }

        @Override
        public String getTitle() {
            return title;
        }

    }


    private static class InternalMementoUnit implements MementoUnit {

        private final RelativePath relativePath;
        private final String title;
        private List<MementoNode> nodeList;

        private InternalMementoUnit(RelativePath relativePath, String title, List<MementoNode> nodeList) {
            this.relativePath = relativePath;
            this.title = title;
            this.nodeList = nodeList;
        }

        @Override
        public String getName() {
            return relativePath.getLastName();
        }

        @Override
        public RelativePath getPath() {
            return relativePath;
        }

        @Override
        public String getTitle() {
            return title;
        }

        @Override
        public boolean isLeaf() {
            return nodeList.isEmpty();
        }

        @Override
        public List<MementoNode> getSubnodeList() {
            return nodeList;
        }

        @Override
        public String getText() {
            return "";
        }

    }


    private static class DocumentInfo {

        private final Lang lang;
        private final String name;
        private final Document document;
        private final String errorMessage;


        private DocumentInfo(Lang lang, String name, Document document) {
            this.lang = lang;
            this.name = name;
            this.document = document;
            this.errorMessage = null;

        }

        private DocumentInfo(Lang lang, String name, String errorMessage) {
            this.lang = lang;
            this.name = name;
            this.document = null;
            this.errorMessage = errorMessage;
        }

        private String transform(SimpleTemplate simpleTemplate) {
            if (errorMessage != null) {
                return "";
            }
            try {
                TransformerParameters transformerParameters = TransformerParameters.build(lang)
                        .check(simpleTemplate.getAttributes());
                return simpleTemplate.transform(new DOMSource(document.getDocumentElement()), transformerParameters.getMap());
            } catch (NestedTransformerException nte) {
                Throwable throwable = nte.getCause();
                return throwable.getClass().getName() + ": " + throwable.getMessage();
            }
        }

        private String getTitle() {
            if (errorMessage != null) {
                return lang.toString() + "/" + name + ".xml / " + errorMessage;
            }
            NodeList nodeList = document.getDocumentElement().getChildNodes();
            int length = nodeList.getLength();
            for (int i = 0; i < length; i++) {
                Node node = nodeList.item(i);
                if (node instanceof Element) {
                    Element el = (Element) node;
                    if (el.getTagName().equals("title")) {
                        String title = DOMUtils.readSimpleElement(el);
                        if (!title.isEmpty()) {
                            return title;
                        }
                    }
                }
            }
            return null;
        }


    }

}
