/* BdfServer - Copyright (c) 2009-2025 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.balayage;

import fr.exemole.bdfserver.api.BdfServer;
import fr.exemole.bdfserver.api.balayage.BalayageTemplate;
import fr.exemole.bdfserver.api.balayage.TemplateProvider;
import fr.exemole.bdfserver.api.configuration.PathConfiguration;
import fr.exemole.bdfserver.api.exportation.transformation.TransformationConstants;
import fr.exemole.bdfserver.api.managers.TransformationManager;
import fr.exemole.bdfserver.tools.BdfURI;
import fr.exemole.bdfserver.tools.exportation.transformation.BdfTransformationUtils;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.text.ParseException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import net.fichotheque.SubsetKey;
import net.fichotheque.corpus.Corpus;
import net.fichotheque.corpus.FicheMeta;
import net.fichotheque.exportation.balayage.BalayageConstants;
import net.fichotheque.exportation.balayage.BalayageDef;
import net.fichotheque.exportation.balayage.BalayageOutput;
import net.fichotheque.exportation.transformation.NoDefaultTemplateException;
import net.fichotheque.exportation.transformation.SimpleTemplate;
import net.fichotheque.exportation.transformation.StreamTemplate;
import net.fichotheque.exportation.transformation.TemplateKey;
import net.fichotheque.exportation.transformation.TransformationKey;
import net.mapeadores.util.exceptions.NestedIOException;
import net.mapeadores.util.exceptions.NestedTransformerException;
import net.mapeadores.util.io.BufferErrorListener;
import net.mapeadores.util.io.IOUtils;
import net.mapeadores.util.mimetype.MimeTypeUtils;
import net.mapeadores.util.text.ValidExtension;


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

    private final static Map<String, String> EMPTY_OUTPUTPROPERTIES = Collections.emptyMap();
    private final ErrorHandler errorHandler;
    private final BdfServer bdfServer;
    private final PathConfiguration pathConfiguration;
    private final BalayageDef balayageDef;
    private URIResolver customURIResolver;

    public TemplateProviderBuilder(BdfServer bdfServer, PathConfiguration pathConfiguration, BalayageDef balayageDef, ErrorHandler errorHandler) {
        this.errorHandler = errorHandler;
        this.bdfServer = bdfServer;
        this.pathConfiguration = pathConfiguration;
        this.balayageDef = balayageDef;
    }

    public void setCustomURIResolver(URIResolver customURIResolver) {
        this.customURIResolver = customURIResolver;
    }

    public TemplateProvider build(BalayageOutput output) {
        String xsltPath = output.getXsltPath();
        if (xsltPath.isEmpty()) {
            errorHandler.undefinedXslPath();
            return null;
        }
        if (xsltPath.startsWith("bdf:")) {
            return buildFromTransformationKey(output, xsltPath.substring(4));
        } else {
            return buildFromBalayage(output, xsltPath);
        }
    }

    private TemplateProvider buildFromBalayage(BalayageOutput output, String xsltPath) {
        Source transformerSource = BdfBalayageUtils.getTransformerSource(bdfServer, balayageDef, xsltPath);
        if (transformerSource == null) {
            errorHandler.wrongXslPath(xsltPath);
            return null;
        }
        BufferErrorListener errorListener = new BufferErrorListener();
        URIResolver uriResolver;
        if (customURIResolver != null) {
            uriResolver = customURIResolver;
        } else {
            uriResolver = BdfURI.getURIResolver(bdfServer, pathConfiguration);
        }
        uriResolver = BdfTransformationUtils.wrap(uriResolver);
        TransformerFactory factory = TransformerFactory.newInstance();
        factory.setURIResolver(uriResolver);
        factory.setErrorListener(errorListener);
        Transformer transformer = null;
        try {
            transformer = factory.newTransformer(transformerSource);
            transformer.setErrorListener(errorListener);
            transformer.setURIResolver(uriResolver);
        } catch (TransformerException te) {
            errorListener.addError(te);
        }
        if (errorListener.hasError()) {
            errorHandler.transformerError(xsltPath, errorListener);
            return null;
        }
        return new XsltTemplateProvider(output, transformer);
    }

    private TemplateProvider buildFromTransformationKey(BalayageOutput output, String templateName) {
        if (!(output.getBalayageUnit().getType().equals(BalayageConstants.FICHE_TYPE))) {
            errorHandler.wrongBalayageUnitType();
            return null;
        }
        String extension = null;
        if (templateName != null) {
            int idx = templateName.indexOf('.');
            if (idx != -1) {
                templateName = templateName.substring(0, idx);
                extension = templateName.substring(idx + 1);
            }
        }
        if ((templateName.length() == 0) || (templateName.equals("_default"))) {
            templateName = null;
        }
        TransformationManager transformationManager = bdfServer.getTransformationManager();
        Map<SubsetKey, TransformationKeyBalayageTemplate> templateMap = new HashMap<SubsetKey, TransformationKeyBalayageTemplate>();
        for (Corpus corpus : bdfServer.getFichotheque().getCorpusList()) {
            SubsetKey corpusKey = corpus.getSubsetKey();
            TransformationKey transformationKey = new TransformationKey(corpusKey);
            if (extension != null) {
                TemplateKey templateKey;
                try {
                    ValidExtension validExtension = ValidExtension.parse(extension);
                    templateKey = TemplateKey.parse(transformationKey, validExtension, templateName);
                } catch (ParseException pe) {
                    return null;
                }
                StreamTemplate streamTemplate;
                try {
                    streamTemplate = transformationManager.getStreamTemplate(templateKey, false);
                    if (streamTemplate == null) {
                        return null;
                    }
                } catch (NoDefaultTemplateException ndte) {
                    return null;
                }
                if (!(streamTemplate instanceof StreamTemplate.Xslt)) {
                    return null;
                }
                templateMap.put(corpusKey, new StreamTransformationKeyBalayageTemplate((StreamTemplate.Xslt) streamTemplate));
            } else {
                TemplateKey templateKey;
                try {
                    templateKey = TemplateKey.parse(transformationKey, templateName);
                } catch (ParseException pe) {
                    return null;
                }
                SimpleTemplate simpleTemplate = transformationManager.getSimpleTemplate(templateKey, false);
                if (simpleTemplate == null) {
                    return null;
                }
                templateMap.put(corpusKey, new SimpleTransformationKeyBalayageTemplate(simpleTemplate));
            }
        }
        return new TransformationKeyTemplateProvider(output, templateMap, bdfServer.getDefaultWorkingLang().toString());
    }


    public interface ErrorHandler {

        public void wrongBalayageUnitType();

        public void transformerError(String xsltPath, BufferErrorListener errorListener);

        public void undefinedXslPath();

        public void wrongXslPath(String xsltPath);

    }


    private static abstract class AbstractTemplateProvider implements TemplateProvider {

        private final Map<String, Object> transformerParameters = new LinkedHashMap<String, Object>();
        private final Set<String> priorParamSet = new HashSet<String>();

        private AbstractTemplateProvider(BalayageOutput output) {
            boolean reversePathDone = false;
            for (BalayageOutput.Param param : output.getParamList()) {
                String name = param.getName();
                if (name.equals(TransformationConstants.BDF_REVERSEPATH_PARAMETER)) {
                    reversePathDone = true;
                } else if (name.equals(TransformationConstants.BDF_FILENAME_PARAMETER)) {
                    setFileName(param.getValue(), false);
                }
                setPriorParam(name, param.getValue());
            }
            if (!reversePathDone) {
                setPriorParam(TransformationConstants.BDF_REVERSEPATH_PARAMETER, output.getReversePath());
            }
        }

        @Override
        public void setCurrentFileName(String fileName) {
            setFileName(fileName, false);
            setFollowingParam(TransformationConstants.BDF_FILENAME_PARAMETER, fileName);
        }

        @Override
        public void setBalayageRoot(String balayageRoot) {
            setFollowingParam(TransformationConstants.BDF_BALAYAGEROOT_PARAMETER, balayageRoot);
        }

        @Override
        public void setCurrentPath(String path) {
            if (path == null) {
                path = "";
            }
            setFollowingParam(TransformationConstants.BDF_PATH_PARAMETER, path);
        }

        @Override
        public void setCurrentLang(String lang) {
            setFollowingParam(TransformationConstants.BDF_LANG_PARAMETER, lang);
        }

        void initParameters(Transformer transformer) {
            for (Map.Entry<String, Object> entry : transformerParameters.entrySet()) {
                transformer.setParameter(entry.getKey(), entry.getValue());
            }
        }

        private void setPriorParam(String name, String value) {
            if (value == null) {
                transformerParameters.remove(name);
                priorParamSet.remove(name);
            } else {
                transformerParameters.put(name, value);
                priorParamSet.add(name);
            }
        }

        Map<String, Object> getTransformerParameters() {
            return transformerParameters;
        }

        private void setFileName(String value, boolean prior) {
            String fileBaseName = null;
            String fileExtension = null;
            if (value != null) {
                fileBaseName = value;
                fileExtension = "";
                int idx = value.lastIndexOf('.');
                if (idx != -1) {
                    fileBaseName = value.substring(0, idx);
                    fileExtension = value.substring(idx + 1);
                }
            }
            if (prior) {
                setPriorParam(TransformationConstants.BDF_FILEBASENAME_PARAMETER, fileBaseName);
                setPriorParam(TransformationConstants.BDF_FILEEXTENSION_PARAMETER, fileExtension);
            } else {
                setPriorParam(TransformationConstants.BDF_FILEBASENAME_PARAMETER, fileBaseName);
                setPriorParam(TransformationConstants.BDF_FILEEXTENSION_PARAMETER, fileExtension);
            }
        }

        void setFollowingParam(String name, String value) {
            if (!priorParamSet.contains(name)) {
                if (value == null) {
                    transformerParameters.remove(name);
                } else {
                    transformerParameters.put(name, value);
                }
            }
        }

    }


    private static class XsltTemplateProvider extends AbstractTemplateProvider {

        private final XsltBalayageTemplate xsltBalayageTemplate;

        XsltTemplateProvider(BalayageOutput output, Transformer transformer) {
            super(output);
            this.xsltBalayageTemplate = new XsltBalayageTemplate(transformer);
        }

        @Override
        public BalayageTemplate getBalayageTemplate(Object extractionObject) {
            initParameters(xsltBalayageTemplate.getTransformer());
            return xsltBalayageTemplate;
        }

    }


    private static class XsltBalayageTemplate implements BalayageTemplate {

        private final Transformer transformer;
        private final String charset;
        private final String mimeType;

        private XsltBalayageTemplate(Transformer transformer) {
            this.transformer = transformer;
            String method = transformer.getOutputProperty("method");
            if (method == null) {
                method = "xml";
            }
            String encoding = transformer.getOutputProperty("encoding");
            if (encoding == null) {
                encoding = "UTF-8";
            }
            this.charset = encoding;
            if (method.equals("xml")) {
                this.mimeType = MimeTypeUtils.XML;
            } else if (method.equals("html")) {
                this.mimeType = MimeTypeUtils.HTML;
            } else {
                this.mimeType = MimeTypeUtils.PLAIN;
            }
        }

        @Override
        public String getMimeType() {
            return mimeType;
        }

        @Override
        public String getCharset() {
            return charset;
        }

        @Override
        public void transform(String extractionString, OutputStream outputStream) {
            StreamSource xmlSource = new StreamSource(new StringReader(extractionString));
            try {
                transformer.transform(xmlSource, new StreamResult(outputStream));
            } catch (TransformerException te) {
                throw new NestedTransformerException(te);
            }
        }

        private Transformer getTransformer() {
            return transformer;
        }

    }


    private static class TransformationKeyTemplateProvider extends AbstractTemplateProvider {

        private final Map<SubsetKey, TransformationKeyBalayageTemplate> templateMap;
        private final String defaultLang;

        TransformationKeyTemplateProvider(BalayageOutput output, Map<SubsetKey, TransformationKeyBalayageTemplate> templateMap, String defaultLang) {
            super(output);
            this.templateMap = templateMap;
            this.defaultLang = defaultLang;
        }

        @Override
        public BalayageTemplate getBalayageTemplate(Object extractionObject) {
            FicheMeta ficheMeta = (FicheMeta) extractionObject;
            SubsetKey corpusKey = ficheMeta.getSubsetKey();
            TransformationKeyBalayageTemplate template = templateMap.get(corpusKey);
            template.update(getTransformerParameters());
            return template;
        }

        @Override
        public void setCurrentLang(String lang) {
            setFollowingParam(TransformationConstants.BDF_LANG_PARAMETER, lang);
            if (lang == null) {
                lang = defaultLang;
            }
            setFollowingParam(TransformationConstants.WORKINGLANG_PARAMETER, lang);
        }

    }


    private static abstract class TransformationKeyBalayageTemplate implements BalayageTemplate {

        Map<String, Object> transformerParameters;
        Map<String, String> outputProperties = new HashMap<String, String>();

        void update(Map<String, Object> transformerParameters) {
            this.transformerParameters = transformerParameters;
        }

    }


    private static class SimpleTransformationKeyBalayageTemplate extends TransformationKeyBalayageTemplate {

        private final SimpleTemplate simpleTemplate;

        private SimpleTransformationKeyBalayageTemplate(SimpleTemplate simpleTemplate) {
            this.simpleTemplate = simpleTemplate;
        }

        @Override
        public String getMimeType() {
            return simpleTemplate.getMimeType();
        }

        @Override
        public String getCharset() {
            return simpleTemplate.getCharset();
        }

        @Override
        public void transform(String extractionString, OutputStream outputStream) {
            String s = simpleTemplate.transform(extractionString, transformerParameters);
            try {
                StringReader reader = new StringReader(s);
                IOUtils.copy(reader, outputStream, simpleTemplate.getCharset());
            } catch (IOException ioe) {
                throw new NestedIOException(ioe);
            }
        }

    }


    private static class StreamTransformationKeyBalayageTemplate extends TransformationKeyBalayageTemplate {

        private final StreamTemplate.Xslt streamTemplate;

        private StreamTransformationKeyBalayageTemplate(StreamTemplate.Xslt streamTemplate) {
            this.streamTemplate = streamTemplate;
        }

        @Override
        public String getMimeType() {
            return streamTemplate.getMimeType();
        }

        @Override
        public String getCharset() {
            return null;
        }

        @Override
        public void transform(String extractionString, OutputStream outputStream) {
            try {
                streamTemplate.transform(extractionString, outputStream, transformerParameters, EMPTY_OUTPUTPROPERTIES);
            } catch (IOException ioe) {
                throw new NestedIOException(ioe);
            }
        }

    }

}
