/*
 * Copyright (c) 2017 XMLmind Software. All rights reserved. 
 *
 * Author: Hussein Shafie
 *
 * This file is part of the XMLmind W2X project.
 * For conditions of distribution and use, see the accompanying legal.txt file.
 */
package com.xmlmind.w2x_servlet;

import java.io.IOException;
import java.io.File;
import java.util.HashMap;
import javax.servlet.ServletException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.xmlmind.multipartreq.MultipartConfig;
import com.xmlmind.multipartreq.Part;
import com.xmlmind.multipartreq.MultipartRequest;
import com.xmlmind.util.ThrowableUtil;
import com.xmlmind.util.StringUtil;
import com.xmlmind.util.FileUtil;
import com.xmlmind.util.Zip;
import com.xmlmind.w2x.processor.Processor;

@SuppressWarnings("serial")
public class WordToXMLServlet extends HttpServlet {
    private static final class ConversionSpec {
        public final String name;
        public final String[] args;
        public final String suffix;

        public ConversionSpec(String name, String[] args, String extension) {
            if (extension != null && extension.startsWith(".")) {
                extension = extension.substring(1);
                if (extension.length() == 0) {
                    extension = null;
                }
            }
            if (extension == null) {
                extension = "xml";
            }

            this.name = name;
            this.args = args;
            suffix = "." + extension;
        }

        public ConversionSpec(ConversionSpec other, String[] args) {
            this.name = other.name;

            String[] args2 = other.args;
            if (args != null) {
                args2 = new String[other.args.length + args.length];
                System.arraycopy(other.args, 0, args2, 0, 
                                 other.args.length);
                System.arraycopy(args, 0, args2, other.args.length, 
                                 args.length);
            }
            this.args = args2;

            this.suffix = other.suffix;
        }
    }

    private RequestLimiter requestLimiter;
    private File baseDir;
    private HashMap<String,ConversionSpec> conversionSpecs;
    private String[] conversionSpecNames;
    private File convertDir;
    private MultipartConfig multipartConfig;

    // -----------------------------------------------------------------------

    @Override
    public void init()
        throws ServletException {
        ServletConfig config = getServletConfig();

        ServletContext context = getServletContext();
        String baseDirName = context.getRealPath("/");
        if (baseDirName != null) {
            baseDir = new File(baseDirName);

            File resourcesDir = new File(baseDir, "w2x_resources");
            if (resourcesDir.isDirectory()) {
                Processor.initResolver(FileUtil.fileToURL(resourcesDir));
            }
        }

        requestLimiter = new RequestLimiter(config, baseDir);

        // ---
        
        // Once initialized, this map is immutable. Hence it's OK to used it
        // from different threads.
        conversionSpecs = new HashMap<String,ConversionSpec>();

        for (int i = 0; i < 100; ++i) {
            String prefix = "conversion" + Integer.toString(i);

            String key = prefix + ".name";
            String name = config.getInitParameter(key);

            if (name != null && (name = name.trim()).length() > 0) {
                key = prefix + ".optionsFile";
                File optionsFile = 
                    ConfUtil.getFile(config, key, null, 0x0, baseDir);

                String[] args;
                try {
                    args = Processor.loadOptionsFile(optionsFile);
                } catch (Exception e) {
                    throw new ServletException(
                        Msg.msg("invalidParameter2", optionsFile.getPath(),
                                key, ThrowableUtil.reason(e)));
                }

                key = prefix + ".outputFileExtension";
                String extension = ConfUtil.getString(config, key, "xml");

                conversionSpecs.put(name, 
                                    new ConversionSpec(name, args, extension));
            }
        }

        conversionSpecNames = 
            conversionSpecs.keySet().toArray(StringUtil.EMPTY_LIST);

        // ---

        File tempDir = (File)
            getServletContext().getAttribute("javax.servlet.context.tempdir");

        File workDir = ConfUtil.getFile(config, "workDir", tempDir.getPath(),
                                        ConfUtil.CREATE|ConfUtil.WRITABLE|
                                        ConfUtil.DIRECTORY, baseDir);
        convertDir = new File(workDir, "conv");
        File uploadDir = new File(workDir, "upload");

        try {
            // Do not make workDir empty as, for example, Jetty unpacks the
            // .war in it.
            FileUtil.doDeleteFileOrDir(convertDir);
            FileUtil.doDeleteFileOrDir(uploadDir);

            FileUtil.checkedMkdir(convertDir);
            FileUtil.checkedMkdir(uploadDir);
        } catch (IOException e) {
            throw new ServletException(Msg.msg("cannotInitWorkDir", workDir, 
                                               ThrowableUtil.reason(e)));
        }

        // "multipart/form-data" requests ---

        int maxRequestSize = 
            ConfUtil.getInt(config, "maxRequestSize", 
                            Integer.toString(20*1024*1024), 
                            1024*1024, Integer.MAX_VALUE);

        int maxFileSize = 
            ConfUtil.getInt(config, "maxFileSize", 
                            Integer.toString(10*1024*1024), 
                                 512*1024, Integer.MAX_VALUE);

        int fileSizeThreshold = 
            ConfUtil.getInt(config, "fileSizeThreshold", "16384", 
                            256, Integer.MAX_VALUE);

        multipartConfig = 
            new MultipartConfig(maxRequestSize, maxFileSize, fileSizeThreshold,
                                uploadDir.getPath());
    }

    // -----------------------------------------------------------------------

    @Override
    protected void service(HttpServletRequest req,
                           HttpServletResponse resp)
        throws ServletException, IOException {
        ServletContext context = getServletContext();
        HttpServletRequest req2 = req;
        Part[] parts = null;

        if (MultipartRequest.isMultipartRequest(req)) {
            MultipartRequest multipartReq = 
                new MultipartRequest(req, multipartConfig, context);
            try {
                parts = multipartReq.getParts();
            } catch (IllegalStateException e) {
                resp.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE,
                               e.getMessage());
                return;
            }

            req2 = multipartReq;
        }

        try {
            super.service(req2, resp);
        } finally {
            if (parts != null) {
                try {
                    for (int i = 0; i < parts.length; ++i) {
                        parts[i].delete();
                    }
                } catch (Exception e) {
                    String reason = e.getClass().getName();
                    if (e.getMessage() != null) {
                        reason += ": " + e.getMessage();
                    }

                    context.log("cannot delete a part of a" +
                                " \"multipart/form-data\" request: " + reason,
                                e);
                }
            }
        }
    }

    // -----------------------------------------------------------------------

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
        ReqUtil.setCharacterEncoding(req);

        // Parse parameter interactive ---

        boolean interactive = ReqUtil.getBoolean(req, "interactive", false);

        // Parse undocumented parameter testing ---

        boolean testing = ReqUtil.getBoolean(req, "testing", false);

        // Parse parameter conv ---

        ConversionSpec conversionSpec = null;

        String conv = ReqUtil.getString(req, "conv", null);
        if (conv != null) {
            conversionSpec = conversionSpecs.get(conv);
        }
        if (conversionSpec == null) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
                           Msg.msg("missingOrUnknownParameter", "conv",
                                   StringUtil.join(", ", conversionSpecNames)));
            return;
        }

        // Parse optional parameter params ---

        String params = ReqUtil.getString(req, "params", null);
        if (params != null) {
            String[] paramList;
            try {
                paramList = Processor.parseOptions(params, /*baseURL*/ null);
            } catch (Exception e) {
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
                               Msg.msg("invalidParameter2", params, "params",
                                       ThrowableUtil.reason(e)));
                return;
            }

            conversionSpec = new ConversionSpec(conversionSpec, paramList);
        }

        // Parse parameter docx ---

        Part docxPart = null;
        // Note that most zip extractors do not support UTF-8 encoded
        // filenames (InfoZip consider them ISO-8859-1 encoded). 
        // However "jar xvf foo.zip" seems to work fine.
        String docxBaseName = ReqUtil.getString(req, "docx", null);
        if (docxBaseName != null) {
            String ext = FileUtil.getExtension(docxBaseName);
            if (ext == null || !"docx".equalsIgnoreCase(ext)) {
                String msg = Msg.msg("notADOCX", docxBaseName);
                if (interactive) {
                    RespUtil.sendAlert("error", msg, null, baseDir, resp);
                } else {
                    resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
                }
                return;
            }

            if (req instanceof MultipartRequest) {
                docxPart = ((MultipartRequest) req).getPart("docx");
            }
        }
        if (docxPart == null) {
            if (interactive) {
                RespUtil.sendAlert("information", Msg.msg("noDOCX"), null, 
                                   baseDir, resp);
            } else {
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST,
                               Msg.msg("missingParameter", "docx"));
            }
            return;
        }

        // Perform the conversion ---

        if (!requestLimiter.allowRequest(req, interactive, resp)) {
            return;
        }

        String requestor = req.getRemoteAddr();
        log("" + requestor + ": converting \"" + docxBaseName + "\" (" +
            Long.toString(docxPart.getSize()) + "bytes) to " + 
            conversionSpec.name);

        File tmpDir = null;
        try {
            File outputFile = null;

            try {
                tmpDir = FileUtil.createTempDirectory("w2x", convertDir);

                File docxFile = new File(tmpDir, docxBaseName);
                docxPart.write(docxFile.getPath());
                // docxFile is something like: w2x_ZZZ_d/foo.docx

                outputFile = convert(docxFile, conversionSpec, testing);
            } catch (Exception e) {
                String msg = 
                    Msg.msg("cannotConvert", docxBaseName, conversionSpec.name,
                            ThrowableUtil.reason(e));
                if (interactive) {
                    RespUtil.sendAlert("error", msg, e, baseDir, resp);
                } else {
                    resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                                   msg);
                }

                outputFile = null;

                log("" + requestor + ": cannot convert \"" + docxBaseName + 
                    "\" to " + conversionSpec.name + ": " + 
                    ThrowableUtil.detailedReason(e));
            }

            if (outputFile != null) {
                RespUtil.sendFile(outputFile, /*contentType*/ null, resp);
            }
        } finally {
            if (tmpDir != null && tmpDir.exists()) {
                FileUtil.doDeleteFileOrDir(tmpDir);
            }
        }
    }

    private File convert(File docxFile, ConversionSpec conversionSpec,
                         boolean testing) 
        throws Exception {
        // docxFile is something like: w2x_ZZZ_d/foo.docx
        File tmpDir = docxFile.getParentFile(); // w2x_ZZZ_d/

        String outName = FileUtil.setExtension(docxFile.getName(), null); // foo

        String outDirName = outName + "-" + conversionSpec.name;
        File outDir = new File(tmpDir, outDirName); // w2x_ZZZ_d/foo-docbook/
        FileUtil.checkedMkdir(outDir);

        Processor processor = new Processor();
        processor.configure(conversionSpec.args);

        boolean isEPUB = ".epub".equalsIgnoreCase(conversionSpec.suffix);
        String outBaseName;
        if (isEPUB && testing) {
            // w2x_ZZZ_d/foo-epub3/foo-epub3.epub
            outBaseName = 
                outName + "-" + conversionSpec.name + conversionSpec.suffix;
        } else {
            // w2x_ZZZ_d/foo-docbook/foo.xml
            // w2x_ZZZ_d/foo-epub3/foo.epub
            outBaseName = outName + conversionSpec.suffix;
        }
        File outFile = new File(outDir, outBaseName);
        processor.process(docxFile, outFile, /*monitor*/ null);

        if (isEPUB) {
            // w2x_ZZZ_d/foo-epub3/foo.epub 
            // OR
            // w2x_ZZZ_d/foo-epub3/foo-epub3.epub
            return outFile;
        } else {
            // w2x_ZZZ_d/foo-docbook.zip
            File zipFile = new File(tmpDir, outDirName + ".zip");
            Zip.zip(outDir, /*includingTopDir*/ true, /*filter*/ null, zipFile);

            return zipFile;
        }
    }
}
