/* BdfServer - Copyright (c) 2021-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.exportation.transformation.compilers;

import fr.exemole.bdfserver.api.BdfServer;
import fr.exemole.bdfserver.api.configuration.PathConfiguration;
import fr.exemole.bdfserver.api.exportation.transformation.StreamTemplateFactory;
import fr.exemole.bdfserver.api.storage.TemplateStorage;
import fr.exemole.bdfserver.tools.exportation.transformation.BdfPictureHandler;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.text.ParseException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import net.fichotheque.Fichotheque;
import net.fichotheque.SubsetKey;
import net.fichotheque.album.Illustration;
import net.fichotheque.exportation.table.Cell;
import net.fichotheque.exportation.transformation.StreamTemplate;
import net.fichotheque.exportation.transformation.TemplateKey;
import net.fichotheque.namespaces.TransformationSpace;
import net.fichotheque.tools.exportation.transformation.TemplateContentDescriptionBuilder;
import net.fichotheque.utils.FichothequeUtils;
import net.mapeadores.opendocument.css.output.StylesConversionEngine;
import net.mapeadores.opendocument.elements.ElementMaps;
import net.mapeadores.opendocument.io.Pictures;
import net.mapeadores.opendocument.transform.PictureHandler;
import net.mapeadores.util.attr.Attribute;
import net.mapeadores.util.io.IOUtils;
import net.mapeadores.util.logging.MessageHandler;
import net.mapeadores.util.mimetype.MimeTypeUtils;
import net.mapeadores.util.text.StringUtils;
import net.mapeadores.util.xml.DefaultXMLWriter;
import net.mapeadores.util.xml.XMLUtils;


/**
 *
 * @author Vincent Calame
 */
public class PropertiesOdStreamTemplateCompiler extends StreamTemplateCompiler {

    private final static String TEMPLATE_ODT = "template.odt";
    private final static String CONTENT_XML = "content.xml";
    private final static String STYLES_XML = "styles.xml";
    private final static String MANIFEST_XML = "META-INF/manifest.xml";

    public PropertiesOdStreamTemplateCompiler(BdfServer bdfServer, PathConfiguration pathConfiguration, TemplateKey templateKey, MessageHandler messageHandler) {
        super(bdfServer, pathConfiguration, templateKey, messageHandler);
    }

    @Override
    public StreamTemplateFactory compile(TemplateStorage.Unit storageUnit) throws CompilationException {
        ReadZipEngine readZipEngine = new ReadZipEngine(storageUnit);
        readZipEngine.run();
        PropertiesStreamTemplate streamTemplate = new PropertiesStreamTemplate(readZipEngine, getTableExportName(storageUnit));
        return new PropertiesStreamTemplateFactory(streamTemplate);
    }


    private class ReadZipEngine {

        private final TemplateStorage.Unit storageUnit;
        private final Map<String, byte[]> map = new LinkedHashMap<String, byte[]>();
        private String contentString = "";
        private String stylesString = "";
        private String manifestString = "";
        private ElementMaps elementMaps;
        private List<Object> contentPartList;
        private List<Object> stylesPartList;


        private ReadZipEngine(TemplateStorage.Unit storageUnit) {
            this.storageUnit = storageUnit;
        }

        private void run() throws CompilationException {
            readZip();
            initDefaultElementMaps();
            insertDefaultStyles();
            contentPartList = toPartList(contentString);
            stylesPartList = toPartList(stylesString);
        }

        private void readZip() throws CompilationException {
            try (ZipInputStream zis = new ZipInputStream(getZipInputStream(TEMPLATE_ODT))) {
                TemplateContentDescriptionBuilder templateContentDescriptionBuilder = newTemplateContentDescriptionBuilder(TEMPLATE_ODT, true, true);
                ZipEntry zipEntry;
                while ((zipEntry = zis.getNextEntry()) != null) {
                    String name = zipEntry.getName();
                    if (name.equals(CONTENT_XML)) {
                        contentString = IOUtils.toString(zis, "UTF-8");
                    } else if (name.equals(STYLES_XML)) {
                        stylesString = IOUtils.toString(zis, "UTF-8");
                    } else if (name.equals(MANIFEST_XML)) {
                        manifestString = IOUtils.toString(zis, "UTF-8");
                    } else {
                        byte[] bytes = IOUtils.toByteArray(zis);
                        map.put(name, bytes);
                    }
                }
            } catch (IOException ioe) {
                addError("_ error.exception.transformation_io", TEMPLATE_ODT, ioe.getMessage());
                throw new CompilationException();
            }
        }

        private void initDefaultElementMaps() {
            String baseURI = TemplateCompilerUtils.getBaseURI(storageUnit.getTemplateKey());
            String stylesCss = "@import url(\"bdf://this/css/_ficheblockelements_odt.css\");";
            elementMaps = parseCss(stylesCss, baseURI, "default_styles.css");
        }

        private void insertDefaultStyles() {
            stylesString = StylesConversionEngine.insertAllStyles(stylesString, elementMaps, false);
        }

        private List<Object> toPartList(String xml) throws CompilationException {
            try {
                return PropertiesOdParser.parse(xml);
            } catch (ParseException pe) {
                throw new CompilationException();
            }
        }

        private InputStream getZipInputStream(String zipName) throws CompilationException {
            InputStream inputStream;
            try {
                inputStream = TemplateCompilerUtils.getContentInputStream(storageUnit, zipName);
            } catch (IOException ioe) {
                addError("_ error.exception.transformation_io", zipName, ioe.getMessage());
                throw new CompilationException();
            }
            if (inputStream == null) {
                addError("_ error.empty.transformation.ressource", zipName);
                throw new CompilationException();
            }
            return inputStream;
        }

    }


    private class PropertiesStreamTemplateFactory implements StreamTemplateFactory {

        private final StreamTemplate streamTemplate;

        private PropertiesStreamTemplateFactory(StreamTemplate streamTemplate) {
            this.streamTemplate = streamTemplate;
        }

        @Override
        public StreamTemplate newInstance() {
            return streamTemplate;
        }

    }


    private class PropertiesStreamTemplate implements StreamTemplate.Properties {

        private final Map<String, byte[]> map;
        private final List<Object> contentPartList;
        private final List<Object> stylesPartList;
        private final String tableExportName;
        private final ElementMaps elementMaps;
        private final String manifestString;

        private PropertiesStreamTemplate(ReadZipEngine readZipEngine, String tableExportName) {
            this.map = readZipEngine.map;
            this.contentPartList = readZipEngine.contentPartList;
            this.stylesPartList = readZipEngine.stylesPartList;
            this.tableExportName = tableExportName;
            this.elementMaps = readZipEngine.elementMaps;
            this.manifestString = readZipEngine.manifestString;
        }

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

        @Override
        public String getTableExportName() {
            return tableExportName;
        }

        @Override
        public void fill(Cell[] cellArray, OutputStream outputStream) throws IOException {
            PictureHandler pictureHandler = new BdfPictureHandler(bdfServer, pathConfiguration);
            ReplaceEngine replaceEngine = new ReplaceEngine(bdfServer, pictureHandler, elementMaps);
            try (ZipOutputStream zos = new ZipOutputStream(outputStream)) {
                zipOtherFiles(zos);
                zipPartList(CONTENT_XML, contentPartList, cellArray, replaceEngine, zos);
                zipPartList(STYLES_XML, stylesPartList, cellArray, replaceEngine, zos);
                Pictures pictures = pictureHandler.toPictures();
                zipPictures(pictures, zos);
                zipManifest(pictures, zos);
            }
        }

        private void zipOtherFiles(ZipOutputStream zos) throws IOException {
            for (Map.Entry<String, byte[]> entry : map.entrySet()) {
                ZipEntry zipEntry = new ZipEntry(entry.getKey());
                zos.putNextEntry(zipEntry);
                IOUtils.write(entry.getValue(), zos);
                zos.closeEntry();
            }
        }

        private void zipPartList(String entryName, List<Object> partList, Cell[] cellArray, ReplaceEngine replaceEngine, ZipOutputStream zos) throws IOException {
            ZipEntry zipEntry = new ZipEntry(entryName);
            zos.putNextEntry(zipEntry);
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(zos, "UTF-8"));
            replaceEngine.setWriter(writer);
            replaceEngine.writePartList(partList, cellArray);
            writer.flush();
            zos.closeEntry();
        }

        private void zipPictures(Pictures pictures, ZipOutputStream zos) throws IOException {
            for (Pictures.Entry picture : pictures.getEntryList()) {
                ZipEntry zipEntry = new ZipEntry(picture.getHref());
                zos.putNextEntry(zipEntry);
                picture.getOdSource().writeStream(zos);
            }
        }

        private void zipManifest(Pictures pictures, ZipOutputStream zos) throws IOException {
            String newManifest = manifestString;
            List<Pictures.Entry> list = pictures.getEntryList();
            if (!list.isEmpty()) {
                int idx = manifestString.indexOf("</manifest:manifest>");
                if (idx != -1) {
                    StringBuilder buf = new StringBuilder(newManifest.substring(0, idx));
                    for (Pictures.Entry entry : list) {
                        buf.append("<manifest:file-entry manifest:media-type=\"\" manifest:full-path=\"");
                        buf.append(entry.getHref());
                        buf.append("\"/>");
                    }
                    buf.append(manifestString.substring(idx));
                    newManifest = buf.toString();
                }
            }
            ZipEntry zipEntry = new ZipEntry(MANIFEST_XML);
            zos.putNextEntry(zipEntry);
            IOUtils.write(newManifest, zos, "UTF-8");
            zos.closeEntry();
        }

    }

    private static String getTableExportName(TemplateStorage.Unit storageUnit) {
        Attribute attribute = storageUnit.getTemplateDef().getAttributes().getAttribute(TransformationSpace.TABLEEXPORT_KEY);
        if (attribute != null) {
            return attribute.getFirstValue();
        } else {
            return "";
        }
    }

    private static String cleanFragment(String text) {
        text = text.replace("</fragment>", "");
        text = text.replaceAll("<fragment.*>", "");
        return text;
    }


    private static class ReplaceEngine {

        private final BdfServer bdfServer;
        private final ElementMaps elementMaps;
        private final PictureHandler pictureHandler;
        private final Fichotheque fichotheque;
        private final ImageXMLPart imageXMLPart;
        private final DefaultXMLWriter xmlWriter;
        private Writer writer;


        private ReplaceEngine(BdfServer bdfServer, PictureHandler pictureHandler, ElementMaps elementMaps) {
            this.bdfServer = bdfServer;
            this.pictureHandler = pictureHandler;
            this.fichotheque = bdfServer.getFichotheque();
            this.xmlWriter = new DefaultXMLWriter();
            this.imageXMLPart = new ImageXMLPart(xmlWriter, pictureHandler);
            this.elementMaps = elementMaps;
        }

        private void setWriter(Writer writer) {
            this.writer = writer;
            xmlWriter.setAppendable(writer);
        }

        private void writePartList(List<Object> partList, Cell[] cellArray) throws IOException {
            for (Object part : partList) {
                if (part instanceof String) {
                    write((String) part);
                } else if (part instanceof InsertPart) {
                    StylesConversionEngine.appendAutomaticStyles(writer, elementMaps);
                } else {
                    PropertyPart propertyPart = (PropertyPart) part;
                    String name = propertyPart.getName();
                    boolean done = false;
                    for (Cell cell : cellArray) {
                        if (cell.getColDef().getColName().equals(name)) {
                            Object value = cell.getValue();
                            if (value != null) {
                                resolve(value, propertyPart);
                            }
                            done = true;
                            break;
                        }
                    }
                    if (!done) {
                        write("????");
                        escape(name);
                        write("????");
                    }
                }
            }
        }

        private void resolve(Object value, PropertyPart propertyPart) throws IOException {
            switch (propertyPart.getMode()) {
                case "raw":
                    writeRaw(value, false);
                    break;
                case "fragment":
                    writeRaw(value, true);
                    break;
                case "image":
                    writeImage(value, propertyPart);
                    break;
                default:
                    writeDefault(value);
            }
        }

        private void writeRaw(Object value, boolean removeFragment) throws IOException {
            String stringValue = value.toString();
            if (removeFragment) {
                stringValue = cleanFragment(stringValue);
            }
            stringValue = pictureHandler.check(stringValue);
            writer.write(stringValue);
        }

        private void writeDefault(Object value) throws IOException {
            if (value instanceof String) {
                String[] tokens = StringUtils.getLineTokens((String) value, StringUtils.NOTCLEAN);
                boolean next = false;
                for (String token : tokens) {
                    if (next) {
                        writer.write("<text:line-break/>");
                    } else {
                        next = true;
                    }
                    XMLUtils.writeEscape(writer, token);
                }
            } else {
                XMLUtils.writeEscape(writer, value.toString());
            }
        }

        private void write(String text) throws IOException {
            writer.write(text);
        }

        private void escape(String text) throws IOException {
            XMLUtils.writeEscape(writer, text);
        }

        private void writeImage(Object value, PropertyPart propertyPart) throws IOException {
            String stringValue = value.toString();
            String[] tokens = StringUtils.getTechnicalTokens(stringValue, true);
            for (String token : tokens) {
                try {
                    Illustration illustration = (Illustration) FichothequeUtils.parseGlobalId(token, fichotheque, SubsetKey.CATEGORY_ALBUM, null);
                    if (illustration != null) {
                        imageXMLPart.write(illustration, propertyPart);
                    }
                } catch (ParseException pe) {

                }
            }
        }

    }

}
