package org.grapheco.elfinder.controller.executors;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.fileupload.FileItemStream;
import org.apache.log4j.Logger;
import org.json.JSONObject;

import org.grapheco.elfinder.controller.MultipleUploadItems;
import org.grapheco.elfinder.controller.executor.AbstractJsonCommandExecutor;
import org.grapheco.elfinder.controller.executor.CommandExecutor;
import org.grapheco.elfinder.controller.executor.FsItemEx;
import org.grapheco.elfinder.service.FsItemFilter;
import org.grapheco.elfinder.service.FsService;

public class UploadCommandExecutor extends AbstractJsonCommandExecutor
		implements CommandExecutor
{
	Logger _logger = Logger.getLogger(this.getClass());

	// large file will be splitted into many parts
	class Part
	{
		long _start;
		long _size;
		FileItemStream _content;

		public Part(long start, long size, FileItemStream fileItemStream)
		{
			super();
			this._start = start;
			this._size = size;
			this._content = fileItemStream;
		}
	}

	// a large file with many parts
	static class Parts
	{
		public static synchronized Parts getOrCreate(
				HttpServletRequest request, String chunkId, String fileName,
				long total, long totalSize)
		{
			//chunkId is not an unique number for files uploaded in one upload form
			String key = String.format("chunk_%s_%s", chunkId, fileName);
			// stores chunks in application context
			Parts parts = (Parts) request.getServletContext().getAttribute(key);

			if (parts == null)
			{
				parts = new Parts(chunkId, fileName, total, totalSize);
				request.getServletContext().setAttribute(key, parts);
			}

			return parts;
		}

		private String _chunkId;
		// number of parts
		private long _numberOfParts;
		private long _totalSize;

		private String _fileName;

		// all chunks
		Map<Long, Part> _parts = new HashMap<Long, Part>();

		public Parts(String chunkId, String fileName, long numberOfParts,
				long totalSize)
		{
			_chunkId = chunkId;
			_fileName = fileName;
			_numberOfParts = numberOfParts;
			_totalSize = totalSize;
		}

		public synchronized void addPart(long partIndex, Part part)
		{
			_parts.put(partIndex, part);
		}

		public boolean isReady()
		{
			return _parts.size() == _numberOfParts;
		}

		public InputStream openInputStream() throws IOException
		{
			return new InputStream()
			{
				long partIndex = 0;
				Part part = _parts.get(partIndex);
				InputStream is = part._content.openStream();

				@Override
				public int read() throws IOException
				{
					while (true)
					{
						// current part is not read completely
						int c = is.read();
						if (c != -1)
						{
							return c;
						}

						// next part?
						if (partIndex == _numberOfParts - 1)
						{
							is.close();
							return -1;
						}

						part = _parts.get(++partIndex);
						is.close();
						is = part._content.openStream();
					}
				}
			};
		}

		public void checkParts() throws IOException
		{
			long totalSize = 0;

			for (long i = 0; i < _numberOfParts; i++)
			{
				Part part = _parts.get(i);
				totalSize += part._size;
			}

			if (totalSize != _totalSize)
				throw new IOException(String.format(
						"invalid file size: excepted %d, but is %d",
						_totalSize, totalSize));
		}

		public void removeFromApplicationContext(HttpServletRequest request)
		{
			String key = String.format("chunk_%s_%s", _chunkId, _fileName);
			request.getServletContext().removeAttribute(key);
		}
	}

	interface FileWriter
	{
		FsItemEx createAndSave(String fileName, InputStream is)
				throws IOException;
	}

	@Override
	public void execute(FsService fsService, HttpServletRequest request,
			ServletContext servletContext, JSONObject json) throws Exception
	{
		MultipleUploadItems uploads = MultipleUploadItems.loadFrom(request);

		final List<FsItemEx> added = new ArrayList<FsItemEx>();

		String target = request.getParameter("target");
		final FsItemEx dir = super.findItem(fsService, target);
		final FsItemFilter filter = getRequestedFilter(request);

		FileWriter fw = new FileWriter()
		{
			@Override
			public FsItemEx createAndSave(String fileName, InputStream is)
					throws IOException
			{
				// fis.getName() returns full path such as 'C:\temp\abc.txt' in
				// IE10
				// while returns 'abc.txt' in Chrome
				// see
				// https://github.com/bluejoe2008/elfinder-2.x-servlet/issues/22
				java.nio.file.Path p = java.nio.file.Paths.get(fileName);
				FsItemEx newFile = new FsItemEx(dir, p.getFileName().toString());

				/*
				 * String fileName = fis.getName(); FsItemEx newFile = new
				 * FsItemEx(dir, fileName);
				 */
				newFile.createFile();
				newFile.writeStream(is);

				if (filter.accepts(newFile))
					added.add(newFile);

				return newFile;
			}
		};

		// chunked upload
		if (request.getParameter("cid") != null)
		{
			processChunkUpload(request, uploads, fw);
		}
		else
		{
			processUpload(uploads, fw);
		}

		json.put("added", files2JsonArray(request, added));
	}

	private void processChunkUpload(HttpServletRequest request,
			MultipleUploadItems uploads, FileWriter fw)
			throws NumberFormatException, IOException
	{
		// cid : unique id of chunked uploading file
		String cid = request.getParameter("cid");
		// solr-5.5.2.tgz.48_65.part
		String chunk = request.getParameter("chunk");

		// 100270176,2088962,136813192
		String range = request.getParameter("range");
		String[] tokens = range.split(",");

		Matcher m = Pattern.compile("(.*)\\.([0-9]+)\\_([0-9]+)\\.part")
				.matcher(chunk);

		if (m.find())
		{
			String fileName = m.group(1);
			long index = Long.parseLong(m.group(2));
			long total = Long.parseLong(m.group(3));

			Parts parts = Parts.getOrCreate(request, cid, fileName, total + 1,
					Long.parseLong(tokens[2]));

			long start = Long.parseLong(tokens[0]);
			long size = Long.parseLong(tokens[1]);

			_logger.debug(String.format("uploaded part(%d/%d) of file: %s",
					index, total, fileName));

			parts.addPart(index, new Part(start, size, uploads
					.items("upload[]").get(0)));
			_logger.debug(String.format(">>>>%d", parts._parts.size()));
			if (parts.isReady())
			{
				parts.checkParts();

				_logger.debug(String.format("file is uploadded completely: %s",
						fileName));

				fw.createAndSave(fileName, parts.openInputStream());

				// remove from application context
				parts.removeFromApplicationContext(request);
			}
		}
	}

	private void processUpload(MultipleUploadItems uploads, FileWriter fw)
			throws IOException
	{
		for (FileItemStream fis : uploads.items("upload[]"))
		{
			fw.createAndSave(fis.getName(), fis.openStream());
		}
	}
}