package org.sigmah.server.servlet;

/*
 * #%L
 * Sigmah
 * %%
 * Copyright (C) 2010 - 2016 URD
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #L%
 */

import com.google.gwt.http.client.Response;
import com.google.inject.Inject;
import com.google.inject.Singleton;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.NoSuchFileException;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Date;
import java.util.Map;
import java.util.UUID;

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

import org.sigmah.client.page.RequestParameter;
import org.sigmah.client.util.ClientUtils;
import org.sigmah.server.dao.FileDAO;
import org.sigmah.server.dao.MonitoredPointDAO;
import org.sigmah.server.dao.OrganizationDAO;
import org.sigmah.server.dao.ProjectDAO;
import org.sigmah.server.domain.Organization;
import org.sigmah.server.domain.Project;
import org.sigmah.server.domain.reminder.MonitoredPoint;
import org.sigmah.server.domain.reminder.MonitoredPointList;
import org.sigmah.server.domain.value.FileVersion;
import org.sigmah.server.file.BackupArchiveManager;
import org.sigmah.server.file.FileStorageProvider;
import org.sigmah.server.file.LogoManager;
import org.sigmah.server.file.util.MultipartRequest;
import org.sigmah.server.file.util.MultipartRequestCallback;
import org.sigmah.server.handler.util.Conflicts;
import org.sigmah.server.service.util.ImageMinimizer;
import org.sigmah.server.servlet.base.AbstractServlet;
import org.sigmah.server.servlet.base.ServletExecutionContext;
import org.sigmah.server.servlet.base.StatusServletException;
import org.sigmah.server.servlet.util.ResponseHelper;
import org.sigmah.shared.dto.reminder.MonitoredPointDTO;
import org.sigmah.shared.dto.value.FileUploadUtils;
import org.sigmah.shared.dto.value.FileVersionDTO;
import org.sigmah.shared.servlet.FileUploadResponse;
import org.sigmah.shared.servlet.ServletConstants.ServletMethod;
import org.sigmah.shared.util.FileType;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * File upload and download servlet.
 * 
 * @author Denis Colliot ([email protected])
 */
@Singleton
public class FileServlet extends AbstractServlet {

	/**
	 * Serial version UID.
	 */
	private static final long serialVersionUID = -8126580127468427311L;

	/**
	 * Logger.
	 */
	private static final Logger LOG = LoggerFactory.getLogger(FileServlet.class);

	/**
	 * Injected application {@link FileStorageProvider}.
	 */
	@Inject
	private FileStorageProvider fileStorageProvider;

	/**
	 * Injected application {@link LogoManager}.
	 */
	@Inject
	private LogoManager logoManager;

	/**
	 * Injected {@link OrganizationDAO}.
	 */
	@Inject
	private OrganizationDAO organizationDAO;

	/**
	 * Injected {@link FileDAO}.
	 */
	@Inject
	private FileDAO fileDAO;

	/**
	 * Injected {@link BackupArchiveManager}.
	 */
	@Inject
	private BackupArchiveManager backupArchiveManager;

	/**
	 * Injected {@link ProjectDAO}.
	 */
	@Inject
	private ProjectDAO projectDAO;

	/**
	 * Injected {@link MonitoredPointDAO}.
	 */
	@Inject
	private MonitoredPointDAO monitoredPointDAO;
	
	/**
	 * Injected {@link Conflicts}.
	 */
	@Inject
	private Conflicts conflicts;

	@Inject
	private ImageMinimizer imageMinimizer;

	// ---------------------------------------------------------------------------------------
	//
	// DOWNLOAD METHODS.
	//
	// ---------------------------------------------------------------------------------------

	/**
	 * See {@link ServletMethod#DOWNLOAD_LOGO} for JavaDoc.
	 * 
	 * @param request
	 *          The HTTP request containing the file id parameter.
	 * @param response
	 *          The HTTP response on which the file content is written.
	 * @param context
	 *          The execution context.
	 * @throws Exception
	 *           If an error occurs during process.
	 */
	protected void downloadLogo(final HttpServletRequest request, final HttpServletResponse response, final ServletExecutionContext context) throws Exception {

		// Retrieves the file id.
		final String id = getParameter(request, RequestParameter.ID, false);

		if (LOG.isDebugEnabled()) {
			LOG.debug("Downloads logo with id '{}'.", id);
		}

		try {

			downloadBase64(id, fileStorageProvider.open(id), response);

		} catch (final NoSuchFileException e) {
			if (LOG.isInfoEnabled()) {
				LOG.info("No logo found for id '" + id + "'.", e);
			}
			throw new StatusServletException(Response.SC_NOT_FOUND, e);
		}
	}

	/**
	 * See {@link ServletMethod#DOWNLOAD_FILE} for JavaDoc.
	 * 
	 * @param request
	 *          The HTTP request containing the file id parameter.
	 * @param response
	 *          The HTTP response on which the file content is written.
	 * @param context
	 *          The execution context.
	 * @throws Exception
	 *           If an error occurs during process.
	 */
	protected void downloadFile(final HttpServletRequest request, final HttpServletResponse response, final ServletExecutionContext context) throws Exception {

		// Retrieves the file version id.
		final Integer fileVersionId = getIntegerParameter(request, RequestParameter.ID, false);

		LOG.debug("Downloads file with version id '{}'.", fileVersionId);

		try {

			final FileVersion version = fileDAO.getVersion(fileVersionId);

			final String name = version.getName() + '.' + version.getExtension();
			final String path = version.getPath();

			download(path, name, fileStorageProvider.open(path), response);

		} catch (final NoSuchFileException e) {
			LOG.info("No file found for version id '" + fileVersionId + "'.", e);
			throw new StatusServletException(Response.SC_NOT_FOUND, e);
		}
	}

	/**
	 * See {@link ServletMethod#DOWNLOAD_ARCHIVE} for JavaDoc.
	 * 
	 * @param request
	 *          The HTTP request containing the file id parameter.
	 * @param response
	 *          The HTTP response on which the file content is written.
	 * @param context
	 *          The execution context.
	 * @throws Exception
	 *           If an error occurs during process.
	 */
	protected void downloadArchive(final HttpServletRequest request, final HttpServletResponse response, final ServletExecutionContext context) throws Exception {

		// Retrieves the file id.
		final String id = getParameter(request, RequestParameter.ID, false);

		if (LOG.isDebugEnabled()) {
			LOG.debug("Downloads archive with id '{}'.", id);
		}

		try {

			download(id, backupArchiveManager.open(id), response);

		} catch (final NoSuchFileException e) {
			if (LOG.isInfoEnabled()) {
				LOG.info("No archive found for id '" + id + "'.", e);
			}
			throw new StatusServletException(Response.SC_NOT_FOUND, e);
		}
	}

	/**
	 * Downloads the given {@code id} file on given {@code response} stream.
	 * 
	 * @param id
	 *          The file id.
	 * @param in
	 *          The file input stream.
	 * @param response
	 *          The HTTP response on which the file content is written.
	 * @throws Exception
	 *           If an error occurs during process.
	 */
	private static void download(final String id, final InputStream in, final HttpServletResponse response) throws Exception {
		download(id, "file_" + id, in, response);
	}

	/**
	 * Downloads the given {@code id} file on given {@code response} stream.
	 * 
	 * @param id
	 *          The file id.
	 * @param fileName
	 *          Name sent in the response header.
	 * @param in
	 *          The file input stream.
	 * @param response
	 *          The HTTP response on which the file content is written.
	 * @throws Exception
	 *           If an error occurs during process.
	 */
	private static void download(final String id, String fileName, final InputStream in, final HttpServletResponse response) throws Exception {

		final FileType fileType = fileTypeFromFileId(id);
		ResponseHelper.executeDownload(response, in, fileType != null ? fileType.getContentType() : null, fileName, null);

	}
	
	private static void downloadBase64(final String id, final InputStream in, final HttpServletResponse response) throws IOException {
		final FileType fileType = fileTypeFromFileId(id);
		ResponseHelper.executeDownload(response, in, fileType != null ? fileType.getContentType() : null, null, null, ResponseHelper.ContentDisposition.BASE64);
	}
	
	private static FileType fileTypeFromFileId(String fileName) {
		final String extension = FilenameUtils.getExtension(fileName);
		return FileType.fromExtension(extension);
	}

	// ---------------------------------------------------------------------------------------
	//
	// UPLOAD METHODS.
	//
	// ---------------------------------------------------------------------------------------

	/**
	 * See {@link ServletMethod#UPLOAD_ORGANIZATION_LOGO} for JavaDoc.
	 * 
	 * @param request
	 *          The HTTP request containing the Organization id parameter.
	 * @param response
	 *          The HTTP response on which the file content is written.
	 * @param context
	 *          The execution context.
	 * @throws java.io.IOException
	 *           If an error occured while reading or writing to the socket or if an error occured while storing the
	 *           uploaded file.
	 * @throws org.sigmah.server.servlet.base.StatusServletException
	 *           If the id parameter was not found or not parseable or if the request type is not MULTIPART or if the file
	 *           exceeded the maximum allowed size.
	 * @throws org.apache.commons.fileupload.FileUploadException
	 *           If an error occured while reading the uploaded file.
	 * @throws javax.servlet.ServletException
	 *           If the given organization could not be found.
	 */
	protected void uploadOrganizationLogo(final HttpServletRequest request, final HttpServletResponse response, final ServletExecutionContext context)
			throws IOException, StatusServletException, ServletException, FileUploadException {

		// --
		// Retrieving parameters from request.
		// --

		final Integer organizationId = getIntegerParameter(request, RequestParameter.ID, false);

		// --
		// Retrieving Organization entity.
		// --

		final Organization organization = organizationDAO.findById(organizationId);
		if (organization == null) {
			throw new ServletException("Cannot find Organization with id '" + organizationId + "'.");
		}

		final String previousLogoFileName = organization.getLogo();

		// --
		// Verifying content length.
		// --

		final int contentLength = request.getContentLength();

		if (contentLength == 0) {
			LOG.error("Empty logo file.");
			throw new StatusServletException(Response.SC_NO_CONTENT);
		}

		if (contentLength > FileUploadUtils.MAX_UPLOAD_IMAGE_SIZE) {
			LOG.error("Logo file's size is too big to be uploaded (size: {}, maximum : {}).", contentLength, FileUploadUtils.MAX_UPLOAD_IMAGE_SIZE);
			throw new StatusServletException(Response.SC_REQUEST_ENTITY_TOO_LARGE);
		}

		// --
		// Saving new logo.
		// --

		organization.setLogo(organization.getId() + "_" + new Date().getTime());
		processUpload(new MultipartRequest(request), response, organization.getLogo(), true, null);
		organizationDAO.persist(organization, context.getUser());

		// --
		// Deleting previous logo file.
		// --

		if (StringUtils.isNotBlank(previousLogoFileName)) {
			fileStorageProvider.delete(previousLogoFileName);
		}

		response.getWriter().write(organization.getLogo());
	}

	/**
	 * See {@link ServletMethod#UPLOAD} for JavaDoc.
	 * 
	 * @param request
	 *          The HTTP request containing the file id parameter.
	 * @param response
	 *          The HTTP response on which the file content is written.
	 * @param context
	 *          The execution context.
	 * @throws Exception
	 *           If an error occurs during process.
	 */
	protected void upload(final HttpServletRequest request, final HttpServletResponse response, final ServletExecutionContext context) throws Exception {

		// --
		// Verify content length.
		// --

		final int contentLength = request.getContentLength();

		if (contentLength == 0) {
			LOG.error("Empty file.");
			throw new StatusServletException(Response.SC_NO_CONTENT);
		}

		if (contentLength > FileUploadUtils.MAX_UPLOAD_FILE_SIZE) {
			LOG.error("File's size is too big to be uploaded (size: {}, maximum : {}).", contentLength, FileUploadUtils.MAX_UPLOAD_FILE_SIZE);
			throw new StatusServletException(Response.SC_REQUEST_ENTITY_TOO_LARGE);
		}
		
		final String fileName = generateUniqueName();

		// --
		// Writing the file.
		// --

		final MultipartRequest multipartRequest = new MultipartRequest(request);
		final long size = this.processUpload(multipartRequest, response, fileName, false, null);
		final Map<String, String> properties = multipartRequest.getProperties();
		
		conflicts.searchForFileAddConflicts(properties, context.getLanguage(), context.getUser());

		// --
		// Create the associated entries in File and FileVersion tables.
		// --

		final Integer fileId = fileDAO.saveOrUpdate(properties, fileName, (int) size);
		final FileVersion fileVersion = fileDAO.getLastVersion(fileId);

		// --
		// If a monitored point must be created.
		// --

		final MonitoredPoint monitoredPoint = parseMonitoredPoint(properties);

		if (monitoredPoint != null) {

			final Integer projectId = ClientUtils.asInt(properties.get(FileUploadUtils.DOCUMENT_PROJECT));
			final Project project = projectDAO.findById(projectId);

			monitoredPoint.setFile(fileDAO.findById(fileId));

			MonitoredPointList list = project.getPointsList();

			if (list == null) {
				list = new MonitoredPointList();
				project.setPointsList(list);
			}

			if (list.getPoints() == null) {
				list.setPoints(new ArrayList<MonitoredPoint>());
			}

			// Adds the point to the list.
			list.addMonitoredPoint(monitoredPoint);

			// Saves monitored point.
			monitoredPointDAO.persist(monitoredPoint, context.getUser());
		}

		final MonitoredPointDTO monitoredPointDTO = mapper().map(monitoredPoint, new MonitoredPointDTO(), MonitoredPointDTO.Mode.BASE);
		final FileVersionDTO fileVersionDTO = mapper().map(fileVersion, new FileVersionDTO());

		response.setContentType(FileType.HTML.getContentType());
		response.getWriter().write(FileUploadResponse.serialize(fileVersionDTO, monitoredPointDTO));
	}

	protected void uploadAvatar(final HttpServletRequest request, final HttpServletResponse response, final ServletExecutionContext context) throws Exception {
		final int contentLength = request.getContentLength();

		if (contentLength == 0) {
			LOG.error("Empty file.");
			throw new StatusServletException(Response.SC_NO_CONTENT);
		}

		if (contentLength > FileUploadUtils.MAX_UPLOAD_FILE_SIZE) {
			LOG.error("File's size is too big to be uploaded (size: {}, maximum : {}).", contentLength, FileUploadUtils.MAX_UPLOAD_FILE_SIZE);
			throw new StatusServletException(Response.SC_REQUEST_ENTITY_TOO_LARGE);
		}

		final String fileName = generateUniqueName();

		// --
		// Writing the file.
		// --

		final MultipartRequest multipartRequest = new MultipartRequest(request);
		processUpload(multipartRequest, response, fileName, false, FileUploadUtils.MAX_AVATAR_SIZE);

		response.setStatus(Response.SC_OK);
		response.setContentType(FileType.TXT.getContentType());
		response.getWriter().write(fileName);
	}

	// ---------------------------------------------------------------------------------------
	//
	// UTILITY METHODS.
	//
	// ---------------------------------------------------------------------------------------

	/**
	 * Processes the file upload.
	 * 
	 * @param request
	 *          The HTTP request.
	 * @param response
	 *          The HTTP response.
	 * @param context
	 *          The execution context.
	 * @param filename
	 *          The uploaded physical file name.
	 * @param logo
	 *          {@code true} if the upload concerns an organization logo, {@code false} otherwise.
	 * @throws java.io.IOException
	 *           If an error occured while reading or writing to the socket or if an error occured while storing the
	 *           uploaded file.
	 * @throws org.sigmah.server.servlet.base.StatusServletException
	 *           if the request type is not MULTIPART or if the file exceeded the maximum allowed size.
	 * @throws org.apache.commons.fileupload.FileUploadException
	 *           If an error occured while reading the uploaded file.
	 */
	private long processUpload(final MultipartRequest multipartRequest, final HttpServletResponse response, final String filename, final boolean logo, final Integer resizeTo)
			throws StatusServletException, IOException, FileUploadException {
		LOG.debug("Starting file uploading...");

		final long[] size = { 0L
		};
		multipartRequest.parse(new MultipartRequestCallback() {

			@Override
			public void onInputStream(InputStream inputStream, String itemName, String mimeType) throws IOException {
				// Retrieving file name.
				// If a name (id) is provided, we use it. If not, using the name of the uploaded file.
				final String name = StringUtils.isNotBlank(filename) ? filename : itemName;

				try (final InputStream stream = inputStream) {
					LOG.debug("Reads image content from the field ; name: '{}'.", name);

					if (logo) {
						size[0] = logoManager.updateLogo(stream, name);
					} else if (resizeTo == null) {
						size[0] = fileStorageProvider.copy(stream, name, StandardCopyOption.REPLACE_EXISTING);
					} else {
						try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
							imageMinimizer.resizeImage(inputStream, byteArrayOutputStream, resizeTo);
							try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray())) {
								size[0] = fileStorageProvider.copy(byteArrayInputStream, name, StandardCopyOption.REPLACE_EXISTING);
							}
						}
					}

					response.setStatus(Response.SC_ACCEPTED);

					// FIXME : perhaps keep the response above for error catching
					// response.getWriter().write("ok");
					LOG.debug("File '{}' upload has been successfully processed.", name);
				}
			}
		});

		return size[0];
	}

	/**
	 * Generates a {@link MonitoredPoint} instance from the given {@code properties}.<br>
	 * Following attributes of the generated monitored point are set:
	 * <ul>
	 * <li>{@code label}</li>
	 * <li>{@code expectedDate}</li>
	 * <li>{@code deleted} (set to {@code false})</li>
	 * </ul>
	 * 
	 * @param properties
	 *          The properties.
	 * @return The monitored point instance.
	 * @throws UnsupportedOperationException
	 *           If the {@code properties} cannot be used to generate a <em>valid</em> {@link MonitoredPoint} instance.
	 */
	private static MonitoredPoint parseMonitoredPoint(final Map<String, String> properties) {

		if (MapUtils.isEmpty(properties)) {
			return null;
		}

		final String label = properties.get(FileUploadUtils.MONITORED_POINT_LABEL);
		final String expectedDateTime = properties.get(FileUploadUtils.MONITORED_POINT_DATE);

		if (StringUtils.isBlank(label) || StringUtils.isBlank(expectedDateTime)) {
			return null;
		}

		try {

			final MonitoredPoint monitoredPoint = new MonitoredPoint();

			monitoredPoint.setLabel(label);
			monitoredPoint.setExpectedDate(new Date(Long.valueOf(expectedDateTime)));
			monitoredPoint.setDeleted(false);

			return monitoredPoint;

		} catch (final Exception e) {
			throw new UnsupportedOperationException("Error occures while generating monitored point from properties.", e);
		}
	}

	/**
	 * Computes and returns a unique string identifier to name files.
	 * 
	 * @return A unique string identifier.
	 */
	public static String generateUniqueName() {
		// Adds the timestamp to ensure the id uniqueness.
		return UUID.randomUUID().toString() + new Date().getTime();
	}

}