package org.atea.nlptools.macroniser.servlets;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.gson.Gson;
import com.google.gson.stream.JsonWriter;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.atea.nlptools.macroniser.monogram.plugin.PluginConfiguration;
import org.atea.nlptools.macroniser.monogram.plugin.PluginManager;
import org.atea.nlptools.macroniser.util.FileUtil;

/**
 * Represents a servlet for macronising the contents of a file.
 * @author University of Waikato - Te Whare Wānanga o Waikato
 * @version 2.0
 * @since 2014-11-20 
 */
public class FileServlet extends HttpServlet {

    private static final Logger logger = LogManager.getLogger(FileServlet.class);

    private final Gson gsonInstance = new Gson();

    private File tmpdir;
    private PluginManager pluginManager;

    @Override
    public void init(ServletConfig config)
        throws ServletException
    {
        super.init(config);

        tmpdir = new File((String)config.getServletContext().getAttribute("tmpdir"));
        pluginManager = new PluginManager(tmpdir);
    }

    /** 
     * Handles the HTTP <code>GET</code> method.
     * @param request The servlet request.
     * @param response The servlet response.
     * @throws IllegalStateException if a reponse has already been committed.
     * @throws IOException if an I/O error occurs.
     */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IllegalStateException, IOException
    {
        sendError(response, HttpStatusCode.MethodNotAllowed, "Expected a POST request with multipart form data.");
    }

    /** 
     * Handles the HTTP <code>POST</code> method.
     * @param request servlet request
     * @param response servlet response
     * @throws ServletException if a servlet-specific error occurs
     * @throws IOException if an I/O error occurs
     */
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws IllegalStateException, IOException, ServletException
    {
        List<FileItem> formItems = null;
        Properties properties = null;
        File restoredFile = null;
        PluginConfiguration configuration = null;

        try
        {
            formItems = parseFormItems(request);
            properties = getRequestProperties(formItems);
        }
        catch (Exception ex)
        {
            throw new ServletException("Expected a multipart/form-data request.", ex);
        }

        try
        {
            Boolean foundFile = getRequestFile(formItems, properties);
            if (!foundFile)
            {
                sendError(response, HttpStatusCode.BadRequest, "A file must be uploaded.");
                return;
            }
        }
        catch (Exception ex)
        {
            logger.error("Failed to get uploaded file", ex);
            sendError(response, HttpStatusCode.InternalServerError, "Failed to retrieve the uploaded file. Please try again.");
            return;
        }

        try
        {
            configuration = configure(properties);

            if (configuration.getFile().length() == 0)
            {
                configuration.getFile().delete();
                sendError(response, HttpStatusCode.InternalServerError, "Failed to retrieve the input file. Please try again.");
                return;
            }

            restoredFile = pluginManager.run(configuration);

            response.setContentType("application/json; charset=UTF-8");
            JsonWriter writer = gsonInstance.newJsonWriter(response.getWriter());
            writer.beginObject();

            writer.name("fileName");
            writer.value(properties.getFileName());

            writer.name("filePath");
            writer.value(restoredFile.getName());

            writer.name("fileType");
            writer.value(configuration.getFileType());
            
            writer.endObject();
            writer.flush();
        }
        catch (UnsupportedOperationException uoex)
        {
            FileUtil.deleteFile(restoredFile);
            sendError(response, HttpStatusCode.UnsupportedMediaType, "Must submit a valid file type.");
        }
        catch (Exception ex)
        {
            FileUtil.deleteFile(restoredFile);
            logger.error("Failed to restore macrons on a file", ex);
            sendError(response, HttpStatusCode.InternalServerError, "Something went wrong. Please try again.");
        }
        finally
        {
            if (configuration != null)
            {
                FileUtil.deleteFile(configuration.getFile());
                // We don't clean up the file held by the properties object,
                // as the configuration holds a reference to said file
            }
        }
    }

    /**
     * Configures the response to contain an error.
     * @param response The response.
     * @param statusCode The HTTP error code.
     * @param description The error description.
     * @throws IOException
     */
    private void sendError(HttpServletResponse response, int statusCode, String description)
        throws IOException
    {
        response.setStatus(statusCode);
        response.getWriter().println(description);
    }

    /**
     * Parses multipart items from a request.
     * @param request The request
     * @return The parsed multipart items.
     * @throws FileUploadException
     */
    private List<FileItem> parseFormItems(HttpServletRequest request)
        throws FileUploadException
    {
        DiskFileItemFactory factory = new DiskFileItemFactory();
        factory.setSizeThreshold(10 * 1024 * 1024);
        ServletFileUpload upload = new ServletFileUpload(factory);

        List<FileItem> items = new ArrayList<>();
        for (Object element : upload.parseRequest(request)) {
            items.add(FileItem.class.cast(element));
        }

        return items;
    }
    
    /**
     * Gets the properties of the request by checking the submitted form items.
     * @param formItems The multipart form items.
     * @return The properties.
     */
    private Properties getRequestProperties(List<FileItem> formItems)
    {
        Properties properties = new Properties();

        for (FileItem item : formItems)
        {
            if (!item.isFormField())
            {
                continue;
            }

            String fieldName = item.getFieldName();
            String fieldValue = item.getString();

            switch (fieldName)
            {
                case "charsetEncoding":
                    properties.setCharsetEncoding(fieldValue);
                    break;
                case "fileType":
                    properties.setFileType(fieldValue);
                    break;
                case "preserveExistingMacrons":
                    properties.setPreserveExistingMacrons(fieldValue);
                    break;
            }
        }

        return properties;
    }

    /**
     * Gets the first file contained in the submitted form items.
     * @param formItems The multipart form items.
     * @return The file, or <code>null</code> if the request contained no files.
     * @throws Exception
     * @throws IOException
     */
    private Boolean getRequestFile(List<FileItem> formItems, Properties properties)
        throws Exception, IOException
    {
        for (FileItem item : formItems)
        {
            if (item.isFormField())
            {
                continue;
            }

            String fileType = FileUtil.guessFileType(new File(item.getName()));
            File file = File.createTempFile(FileUtil.TMP_FILE_PREFIX, fileType, tmpdir);
            item.write(file);

            properties.setFile(file);
            properties.setFilename(item.getName());

            return true;
        }

        return false;
    }
    
    /* Useful for debugging file operations */
    // private String readStringFromFile(File file)
    //     throws FileNotFoundException, IOException
    // {
    //     BufferedReader reader = new BufferedReader(new FileReader(file));
    //     String line = null;
    //     StringBuilder sb = new StringBuilder();

    //     try
    //     {
    //         while ((line=reader.readLine()) != null)
    //         {
    //             sb.append(line);
    //         }
    //     }
    //     finally
    //     {
    //         reader.close();
    //     }

    //     return sb.toString();
    // }
    
    private PluginConfiguration configure(Properties properties)
    {
        final File file = properties.getFile();

        String fileType = properties.getFileType();
        if (fileType == null) {
            fileType = FileUtil.guessFileType(file);
        }

        String charsetEncoding = properties.getCharsetEncoding();
        if (charsetEncoding == null) {
            charsetEncoding = "utf8";
        }

        final PluginConfiguration configuration = new PluginConfiguration();
        configuration.setFile(file);
        configuration.setFileType(fileType);
        configuration.setCharsetEncoding(charsetEncoding);
        configuration.setPreserveExistingMacrons(properties.getPreserveExistingMacrons());

        return configuration;
    }

    private class Properties
    {
        private File file;
        private String filename;
        private String fileType;
        private String charsetEncoding;
        private Boolean preserveExistingMacrons;

        public File getFile() {
            return file;
        }

        public String getFileName() {
            return filename;
        }

        public String getFileType() {
            return fileType;
        }

        public String getCharsetEncoding() {
            return charsetEncoding;
        }

        public Boolean getPreserveExistingMacrons() {
            return preserveExistingMacrons;
        }

        public void setFile(File file) {
            this.file = file;
        }

        public void setFilename(String filename) {
            this.filename = filename;
        }

        public void setFileType(String fileType) {
            this.fileType = fileType;
        }

        public void setCharsetEncoding(String charsetEncoding) {
            this.charsetEncoding = charsetEncoding;
        }

        public void setPreserveExistingMacrons(String preserveExistingMacrons) {
            this.preserveExistingMacrons = Boolean.parseBoolean(preserveExistingMacrons);
        }
    }
}
