/*
 * LinShare is an open source filesharing software, part of the LinPKI software
 * suite, developed by Linagora.
 * 
 * Copyright (C) 2015 LINAGORA
 * 
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version, provided you comply with the Additional Terms applicable for
 * LinShare software by Linagora pursuant to Section 7 of the GNU Affero General
 * Public License, subsections (b), (c), and (e), pursuant to which you must
 * notably (i) retain the display of the “LinShare™” trademark/logo at the top
 * of the interface window, the display of the “You are using the Open Source
 * and free version of LinShare™, powered by Linagora © 2009–2015. Contribute to
 * Linshare R&D by subscribing to an Enterprise offer!” infobox and in the
 * e-mails sent with the Program, (ii) retain all hypertext links between
 * LinShare and linshare.org, between linagora.com and Linagora, and (iii)
 * refrain from infringing Linagora intellectual property rights over its
 * trademarks and commercial brands. Other Additional Terms apply, see
 * <http://www.linagora.com/licenses/> for more details.
 * 
 * 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 Affero General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Affero General Public License and
 * its applicable Additional Terms for LinShare along with this program. If not,
 * see <http://www.gnu.org/licenses/> for the GNU Affero General Public License
 * version 3 and <http://www.linagora.com/licenses/> for the Additional Terms
 * applicable to LinShare software.
 */
package org.linagora.linshare.webservice.uploadrequest.impl;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentMap;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;

import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.Validate;
import org.apache.cxf.jaxrs.ext.multipart.Multipart;
import org.apache.cxf.jaxrs.ext.multipart.MultipartBody;
import org.linagora.linshare.core.exception.BusinessException;
import org.linagora.linshare.core.facade.webservice.uploadrequest.UploadRequestUrlFacade;
import org.linagora.linshare.webservice.WebserviceBase;
import org.linagora.linshare.webservice.dto.ErrorDto;
import org.linagora.linshare.webservice.uploadrequest.FlowUploaderRestService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.wordnik.swagger.annotations.Api;

@Path("/flow/upload")
@Api(value = "/rest/uploadrequest/flow/upload", description = "upload_requests API")
@Produces({ "application/json", "application/xml" })
public class FlowUploaderRestServiceImpl extends WebserviceBase implements
		FlowUploaderRestService {

	private static final Logger logger = LoggerFactory
			.getLogger(FlowUploaderRestServiceImpl.class);

	private static final String CHUNK_NUMBER = "flowChunkNumber";
	private static final String TOTAL_CHUNKS = "flowTotalChunks";
	private static final String CHUNK_SIZE = "flowChunkSize";
	private static final String TOTAL_SIZE = "flowTotalSize";
	private static final String IDENTIFIER = "flowIdentifier";
	private static final String FILENAME = "flowFilename";
	private static final String RELATIVE_PATH = "flowRelativePath";
	private static final String FILE = "file";
	private static final String PASSWORD = "password";
	private static final String REQUEST_URL_UUID = "requestUrlUuid";
	private static final String IEFILENAME = "filename";

	private final UploadRequestUrlFacade uploadRequestUrlFacade;

	private static final ConcurrentMap<String, ChunkedFile> chunkedFiles = Maps
			.newConcurrentMap();

	private class ChunkedFile {

		private final ArrayList<Long> chunks = Lists.newArrayList();
		private final java.nio.file.Path path;

		private ChunkedFile(java.nio.file.Path path) {
			this.path = path;
		}

		public boolean hasChunk(long chunkNumber) {
			return chunks.contains(chunkNumber);
		}

		public void addChunk(long chunkNumber) {
			chunks.add(chunkNumber);
		}

		public ArrayList<Long> getChunks() {
			return chunks;
		}

		public java.nio.file.Path getPath() {
			return path;
		}
	}

	public FlowUploaderRestServiceImpl(
			UploadRequestUrlFacade uploadRequestUrlFacade) {
		super();
		this.uploadRequestUrlFacade = uploadRequestUrlFacade;
	}

	@Path("/")
	@GET
	@Override
	public Response testChunk(@QueryParam(CHUNK_NUMBER) long chunkNumber,
			@QueryParam(TOTAL_CHUNKS) long totalChunks,
			@QueryParam(CHUNK_SIZE) long chunkSize,
			@QueryParam(TOTAL_SIZE) long totalSize,
			@QueryParam(IDENTIFIER) String identifier,
			@QueryParam(FILENAME) String filename,
			@QueryParam(RELATIVE_PATH) String relativePath) {

		identifier = cleanIdentifier(identifier);
		Validate.isTrue(isValid(chunkNumber, chunkSize, totalSize, identifier,
				filename));

		if (chunkedFiles.containsKey(identifier)
				&& chunkedFiles.get(identifier).hasChunk(chunkNumber)) {
			return Response.ok().build();
		}
		return Response.status(Status.NOT_FOUND).build();
	}

	@Path("/")
	@POST
	@Consumes("multipart/form-data")
	@Override
	public Response uploadChunk(@Multipart(CHUNK_NUMBER) long chunkNumber,
			@Multipart(TOTAL_CHUNKS) long totalChunks,
			@Multipart(CHUNK_SIZE) long chunkSize,
			@Multipart(TOTAL_SIZE) long totalSize,
			@Multipart(IDENTIFIER) String identifier,
			@Multipart(FILENAME) String filename,
			@Multipart(RELATIVE_PATH) String relativePath,
			@Multipart(FILE) InputStream file, MultipartBody body,
			@Multipart(REQUEST_URL_UUID) String uploadRequestUrlUuid,
			@Multipart(PASSWORD) String password) throws BusinessException {

		logger.debug("upload chunk number : " + chunkNumber);
		identifier = cleanIdentifier(identifier);
		Validate.isTrue(isValid(chunkNumber, chunkSize, totalSize, identifier,
				filename));
		try {
			logger.debug("writing chunk number : " + chunkNumber);
			java.nio.file.Path tempFile = getTempFile(identifier);
			FileChannel fc = FileChannel.open(tempFile,
					StandardOpenOption.CREATE, StandardOpenOption.APPEND);
			byte[] byteArray = IOUtils.toByteArray(file);
			fc.write(ByteBuffer.wrap(byteArray), (chunkNumber - 1) * chunkSize);
			fc.close();
			chunkedFiles.get(identifier).addChunk(chunkNumber);
			if (isUploadFinished(identifier, chunkSize, totalSize)) {
				logger.debug("upload finished ");
				InputStream inputStream = Files.newInputStream(tempFile,
						StandardOpenOption.READ);
				uploadRequestUrlFacade.addUploadRequestEntry(
						uploadRequestUrlUuid, password, inputStream, filename);
				ChunkedFile remove = chunkedFiles.remove(identifier);
				Files.deleteIfExists(remove.getPath());
				return Response.ok("upload success").build();
			} else {
				logger.debug("upload pending ");
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return Response.ok("upload success").build();
	}

	private java.nio.file.Path getTempFile(String identifier)
			throws IOException {
		ChunkedFile chunkedFile = chunkedFiles.get(identifier);
		if (chunkedFile == null) {
			java.nio.file.Path path = Files.createTempFile("ls-chunks-"
					+ identifier, ".temp");
			chunkedFiles.putIfAbsent(identifier, new ChunkedFile(path));
			chunkedFile = chunkedFiles.get(identifier);
		}
		return chunkedFile.getPath();
	}

	/**
	 * UPLOAD FOR IE
	 */

	@Path("/iexplorer")
	@POST
	@Consumes(MediaType.MULTIPART_FORM_DATA)
	@Produces(MediaType.TEXT_PLAIN)
	@Override
	public Response uploadForIe9(@Multipart(value = FILE) InputStream file,
			MultipartBody body,
			@Multipart(REQUEST_URL_UUID) String uploadRequestUrlUuid,
			@Multipart(PASSWORD) String password) throws BusinessException {
		if (file == null) {
			throw giveRestException(HttpStatus.SC_BAD_REQUEST,
					"Missing file (check parameter file)");
		}
		// Ensure fileName and description aren't null
		String fileName = body.getAttachment(FILE).getContentDisposition()
				.getParameter(IEFILENAME);

		try {
			byte[] bytes = fileName.getBytes("ISO-8859-1");
			fileName = new String(bytes, "UTF-8");
		} catch (UnsupportedEncodingException e1) {
			logger.error("Can not encode file name " + e1.getMessage());
		}

		ErrorDto errorDto;
		try {
			uploadRequestUrlFacade.addUploadRequestEntry(uploadRequestUrlUuid,
					password, file, fileName);
			errorDto = new ErrorDto(0, "upload success");
		} catch (BusinessException exception) {
			logger.error(exception.getMessage());
			errorDto = new ErrorDto(exception.getErrorCode().getCode(),
					exception.getMessage());
		}
		ResponseBuilder response = Response.status(Status.OK);
		response.entity(errorDto);
		return response.build();
	}

	/**
	 * HELPERS
	 */

	private String cleanIdentifier(String identifier) {
		return identifier.replaceAll("[^0-9A-Za-z_-]", "");
	}

	private boolean isValid(long chunkNumber, long chunkSize, long totalSize,
			String identifier, String filename) {
		// Check if the request is sane
		if (chunkNumber == 0 || chunkSize == 0 || totalSize == 0
				|| identifier.length() == 0 || filename.length() == 0) {
			return false;
		}
		double numberOfChunks = computeNumberOfChunks(chunkSize, totalSize);
		if (chunkNumber > numberOfChunks) {
			return false;
		}
		return true;
	}

	private double computeNumberOfChunks(long chunkSize, long totalSize) {
		return Math.max(Math.floor(totalSize / chunkSize), 1);
	}

	private boolean isUploadFinished(String identifier, long chunkSize,
			long totalSize) {
		double numberOfChunks = computeNumberOfChunks(chunkSize, totalSize);
		return chunkedFiles.get(identifier).getChunks().size() == numberOfChunks;
	}
}
