/* global define */

(function(factory) {
	if (typeof define === "function" && define.amd) {
		define([
			"jquery",
			"./core"
		], factory);
	} else {
		factory(jQuery, Formstone);
	}
}(function($, Formstone) {

	"use strict";

	/**
	 * @method private
	 * @name setup
	 * @description Setup plugin.
	 */

	function setup() {
		var blobSliceMethods = [
				'mozSlice',
				'webkitSlice',
				'slice'
			];

		if (Formstone.support.file) {
			var testFile = false;

			try {
				testFile = new File([""], "f");
			} catch (e) {}

			if (!testFile) {
				// Safari & IE/Edge :/
				try {
					testFile = new Blob([""], {});
				} catch (e) {}
			}

			if (testFile) {
				for (var i in blobSliceMethods) {
					if (blobSliceMethods.hasOwnProperty(i) && blobSliceMethods[i] in testFile) {
						BlobSliceMethod = blobSliceMethods[i];
						break;
					}
				}
			}
		}
	}

	/**
	 * @method private
	 * @name construct
	 * @description Builds instance.
	 * @param data [object] "Instance data"
	 */

	function construct(data) {
		if (Formstone.support.file) {
			var html = "";

			if (!BlobSliceMethod) {
				data.chunked = false;
			}

			// Backwards compat
			if (data.maxQueue) {
				data.maxConcurrent = data.maxQueue;
			}

			if (data.label !== false) {
				html += '<div class="' + RawClasses.target + '">';
				html += data.label;
				html += '</div>';
			}
			html += '<input class="' + RawClasses.input + '" type="file"';
			if (data.multiple) {
				html += ' multiple';
			}
			if (data.accept) {
				html += ' accept="' + data.accept + '"';
			}
			html += '>';

			data.baseClasses = [RawClasses.base, data.theme, data.customClass].join(" ");

			this.addClass(data.baseClasses)
				.append(html);

			data.$input       = this.find(Classes.input);
			data.queue        = [];
			data.total        = 0;
			data.uploaded     = 0;
			data.uploading    = false;
			data.disabled     = true;
			data.aborting     = false;

			this.on(Events.click, Classes.target, data, onClick)
				.on(Events.dragEnter, data, onDragEnter)
				.on(Events.dragOver, data, onDragOver)
				.on(Events.dragLeave, data, onDragOut)
				.on(Events.drop, data, onDrop);

			data.$input.on(Events.focus, data, onFocus)
					   .on(Events.blur, data, onBlur)
					   .on(Events.change, data, onChange);

			enableUpload.call(this, data);
		}
	}

	/**
	 * @method private
	 * @name destruct
	 * @description Tears down instance.
	 * @param data [object] "Instance data"
	 */

	function destruct(data) {
		if (Formstone.support.file) {
			data.$input.off(Events.namespace);

			this.off(Events.namespace)
				.removeClass(data.baseClasses)
				.html("");
		}
	}

	/**
	 * @method private
	 * @name abortUpload
	 * @description Cancels all active uploads.
	 * @param data [object] "Instance data"
	 * @param index [int] "File index"
	 */

	/**
	 * @method
	 * @name abort
	 * @description Cancels all active uploads.
	 * @example $(".target").upload("abort");
	 */

	function abortUpload(data, index) {
		var file;

		data.aborting = true;

		for (var i in data.queue) {
			if (data.queue.hasOwnProperty(i)) {
				file = data.queue[i];

				if ($.type(index) === "undefined" || (index >= 0 && file.index === index)) {
					// Abort all
					if (file.started && !file.complete) {
						if (data.chunked) {
							file.chunkTransfer.abort();
						} else {
							file.transfer.abort();
						}
					} else {
						abortFile(data, file, "abort");
					}
				}
			}
		}

		data.aborting = false;

		checkQueue(data);
	}

	/**
	 * @method private
	 * @name abortFile
	 * @description Aborts file.
	 * @param data [object] "Instance data"
	 * @param formData [object] "Target form"
	 * @param file [object] "Target file"
	 */

	function abortFile(data, file, error) {
		file.error = true;
		data.$el.trigger(Events.fileError, [ file, error ]);

		if (!data.aborting) {
			checkQueue(data);
		}
	}

	/**
	 * @method private
	 * @name abortChunk
	 * @description Aborts file chunk.
	 * @param data [object] "Instance data"
	 * @param formData [object] "Target form"
	 * @param file [object] "Target file"
	 */

	function abortChunk(data, file, error) {
		data.$el.trigger(Events.chunkError, [ file, error ]);

		abortFile(data, file, error);
	}

	/**
	 * @method
	 * @name disable
	 * @description Disables target instance.
	 * @example $(".target").upload("disable");
	 */

	function disableUpload(data) {
		if (!data.disabled) {
			this.addClass(RawClasses.disabled);
			data.$input.prop("disabled", true);

			data.disabled = true;
		}
	}

	/**
	 * @method
	 * @name enable
	 * @description Enables target instance.
	 * @example $(".target").upload("enable");
	 */

	function enableUpload(data) {
		if (data.disabled) {
			this.removeClass(RawClasses.disabled);
			data.$input.prop("disabled", false);

			data.disabled = false;
		}
	}

	/**
	 * @method private
	 * @name onClick
	 * @description Handles click to target.
	 * @param e [object] "Event data"
	 */

	function onClick(e) {
		Functions.killEvent(e);

		var data = e.data;

		if (!data.disabled) {
			data.$input.trigger(Events.click);
		}
	}

	/**
	 * @method private
	 * @name onFocus
	 * @description Handles instance focus
	 * @param e [object] "Event data"
	 */

	function onFocus(e) {
		e.data.$el.addClass(RawClasses.focus);
	}

	/**
	 * @method private
	 * @name onBlur
	 * @description Handles instance blur
	 * @param e [object] "Event data"
	 */

	function onBlur(e) {
		e.data.$el.removeClass(RawClasses.focus);
	}

	/**
	 * @method private
	 * @name onChange
	 * @description Handles change to hidden input.
	 * @param e [object] "Event data"
	 */

	function onChange(e) {
		Functions.killEvent(e);

		var data = e.data,
			files = data.$input[0].files;

		if (!data.disabled && files.length) {
			handleUpload(data, files);
		}
	}

	/**
	 * @method private
	 * @name onDragEnter
	 * @description Handles dragenter to target.
	 * @param e [object] "Event data"
	 */

	function onDragEnter(e) {
		Functions.killEvent(e);

		var data = e.data;

		// if (!data.disabled) {
			data.$el.addClass(RawClasses.dropping)
					.trigger(Events.fileDragEnter);
		// }
	}

	/**
	 * @method private
	 * @name onDragOver
	 * @description Handles dragover to target.
	 * @param e [object] "Event data"
	 */

	function onDragOver(e) {
		Functions.killEvent(e);

		var data = e.data;

		// if (!data.disabled) {
			data.$el.addClass(RawClasses.dropping)
					.trigger(Events.fileDragOver);
		// }
	}

	/**
	 * @method private
	 * @name onDragOut
	 * @description Handles dragout to target.
	 * @param e [object] "Event data"
	 */

	function onDragOut(e) {
		Functions.killEvent(e);

		var data = e.data;

		// if (!data.disabled) {
			data.$el.removeClass(RawClasses.dropping)
					.trigger(Events.fileDragLeave);
		// }
	}

	/**
	 * @method private
	 * @name onDrop
	 * @description Handles drop to target.
	 * @param e [object] "Event data"
	 */

	function onDrop(e) {
		Functions.killEvent(e);

		var data = e.data,
			files = e.originalEvent.dataTransfer.files;

		data.$el.removeClass(RawClasses.dropping);

		if (!data.disabled) {
			handleUpload(data, files);
		}
	}

	/**
	 * @method private
	 * @name handleUpload
	 * @description Handles new files.
	 * @param data [object] "Instance data"
	 * @param files [object] "File list"
	 */

	function handleUpload(data, files) {
		var newFiles = [],
			numFiles = files.length;

		if (data.maxFiles) {
			var filesRemaining = data.maxFiles - data.uploaded;

			if (filesRemaining >= 0 && files.length > filesRemaining) {
				numFiles = filesRemaining;
			}
		}

		if (numFiles > 0) {
			for (var i = 0; i < numFiles; i++) {
				var file = {
					index       : data.total++,
					file        : files[i],
					name        : files[i].name,
					size        : files[i].size,
					started     : false,
					complete    : false,
					error       : false,
					transfer    : null
				};

				newFiles.push(file);
				data.queue.push(file);
			}

			data.$el.trigger(Events.queued, [ newFiles ]);

			if (data.autoUpload) {
				startUpload(data);
			}
		}

		data.$input.val("");
	}

	/**
	 * @method private
	 * @name startUpload
	 * @description Start queued uploads.
	 * @param data [object] "Instance data"
	 */

	/**
	 * @method
	 * @name start
	 * @description Starts queued uploads; Use when autoUpload is set to false.
	 * @example $(".target").upload("start");
	 */

	function startUpload(data) {
		if (!data.uploading) {
			$Window.on(Events.beforeUnload, function() {
				return data.leave;
			});

			data.uploading = true;

			data.$el.trigger(Events.start, [ data.queue ]);
		}

		checkQueue(data);
	}

	/**
	 * @method private
	 * @name checkQueue
	 * @description Checks and updates file queue.
	 * @param data [object] "Instance data"
	 */

	function checkQueue(data) {
		var transfering = 0,
			newQueue = [];

		// remove lingering items from queue
		for (var i in data.queue) {
			if (data.queue.hasOwnProperty(i) && !data.queue[i].complete && !data.queue[i].error) {
				newQueue.push(data.queue[i]);
			}
		}

		data.queue = newQueue;

		for (var j in data.queue) {
			if (data.queue.hasOwnProperty(j)) {
				if (!data.queue[j].started) {
					uploadFile(data, data.queue[j]);
				}

				transfering++;

				if (transfering >= data.maxConcurrent) {
					return;
				} else {
					i++;
				}
			}
		}

		if (transfering === 0) {
			$Window.off(Events.beforeUnload);

			data.uploading = false;

			data.$el.trigger(Events.complete);
		}
	}

	/**
	 * @method private
	 * @name uploadFile
	 * @description Uploads file.
	 * @param data [object] "Instance data"
	 * @param file [object] "Target file"
	 */

	function uploadFile(data, file) {
		if (file.size >= data.maxSize || file.error === true) {
			abortFile(data, file, "size");
		} else if (data.chunked) {
			// Chunked upload
			file.started = true;

			file.chunkSize = (1024 * data.chunkSize);
			file.totalChunks = Math.ceil( file.size / file.chunkSize );
			file.currentChunk = 0;

			data.$el.trigger(Events.fileStart, [ file ]);

			uploadChunk(data, file);
		} else {
			var formData = new FormData();

			formData.append(data.postKey, file.file);
			formData = setFormData(data, formData, file);

			if (formData === false) {
				abortFile(data, file, "abort");
			} else {
				// Standard upload
				file.started = true;
				file.transfer = $.ajax({
					url         : data.action,
					data        : formData,
					dataType    : data.dataType,
					type        : "POST",
					contentType : false,
					processData : false,
					cache       : false,
					xhr: function() {
						var $xhr = $.ajaxSettings.xhr();

						if ($xhr.upload) {
							// Clean progress event
							$xhr.upload.addEventListener("progress", function(e) {
								var percent = 0,
									position = e.loaded || e.position,
									total = e.total;

								if (e.lengthComputable) {
									percent = Math.ceil((position / total) * 100);
								}

								data.$el.trigger(Events.fileProgress, [ file, percent ]);
							}, false);
						}

						return $xhr;
					},
					beforeSend: function(jqXHR, settings) {
						data.$el.trigger(Events.fileStart, [ file ]);
					},
					success: function(response, status, jqXHR) {
						file.complete = true;

						data.uploaded++;
						data.$el.trigger(Events.fileComplete, [ file, response ]);

						checkQueue(data);
					},
					error: function(jqXHR, status, error) {
						abortFile(data, file, error);
					}
				});
			}
		}
	}

	/**
	 * @method private
	 * @name uploadChunk
	 * @description Uploads file chunk.
	 * @param data [object] "Instance data"
	 * @param formData [object] "Target form"
	 * @param file [object] "Target file"
	 */

	function uploadChunk(data, file) {
		var chunkStart = (file.chunkSize * file.currentChunk),
			chunkEnd   = (chunkStart + file.chunkSize);

		if (chunkEnd > file.size) {
			chunkEnd = file.size;
		}

		var newChunk = file.file[ BlobSliceMethod ]( chunkStart, chunkEnd ),
			formData = new FormData();

		formData.append(data.postKey, newChunk, file.file.name);
		formData.append("chunks", file.totalChunks);
		formData.append("chunk", file.currentChunk);

		formData = setFormData(data, formData, file);

		if (formData === false) {
			abortFile(data, file, "abort");
		} else {
			file.chunkTransfer = $.ajax({
				url         : data.action,
				data        : formData,
				dataType    : data.dataType,
				type        : "POST",
				contentType : false,
				processData : false,
				cache       : false,
				beforeSend: function(jqXHR, settings) {
					data.$el.trigger(Events.chunkStart, [ file ]);
				},
				success: function(response, status, jqXHR) {
					file.currentChunk++;

					data.$el.trigger(Events.chunkComplete, [ file ]);

					var percent = Math.ceil((file.currentChunk / file.totalChunks) * 100);
					data.$el.trigger(Events.fileProgress, [ file, percent ]);

					if (file.currentChunk < file.totalChunks) {
						uploadChunk(data, file);
					} else {
						file.complete = true;
						data.$el.trigger(Events.fileComplete, [ file, response ]);

						checkQueue(data);
					}
				},
				error: function(jqXHR, status, error) {
					abortChunk(data, file, error);
				}
			});
		}
	}

	function setFormData(data, formData, file) {
		for (var i in data.postData) {
			if (data.postData.hasOwnProperty(i)) {
				formData.append(i, data.postData[i]);
			}
		}

		// Modify data before upload
		formData = data.beforeSend.call(data.$el, formData, file);

		return formData;
	}

	/**
	 * @plugin
	 * @name Upload
	 * @description A jQuery plugin for simple drag and drop uploads.
	 * @type widget
	 * @main upload.js
	 * @main upload.css
	 * @dependency jQuery
	 * @dependency core.js
	 */

	var Plugin = Formstone.Plugin("upload", {
			widget: true,

			/**
			 * @options
			 * @param accept [string] "Input accept attribute"
			 * @param action [string] "Where to submit uploads"
			 * @param autoUpload [boolean] <false> "Beging upload when files are dropped"
			 * @param beforeSend [function] "Run before request sent, must return modified formdata or `false` to cancel"
			 * @param chunked [boolean] <false> "Use chunked uploading, if supported"
			 * @param chunkSize [int] <100> "Size to chunk, in kB"
			 * @param customClass [string] <''> "Class applied to instance"
			 * @param dataType [string] <'html'> "Data type of AJAX request"
			 * @param label [string] <'Drag and drop files or click to select'> "Drop target text; `false` to disable"
			 * @param leave [string] <'You have uploads pending, are you sure you want to leave this page?'> "Before leave message"
			 * @param maxConcurrent [int] <2> "Number of files to simultaneously upload"
			 * @param maxFiles [int OR boolean] <false> "Total number of files that can be uploaded; `false` to disable"
			 * @param maxSize [int] <5242880> "Max file size allowed"
			 * @param multiple [true] <true> "Flag to allow mutiple file uploads"
			 * @param postData [object] "Extra data to post with upload"
			 * @param postKey [string] <'file'> "Key to upload file as"
			 * @param theme [string] <"fs-light"> "Theme class name"
			 */

			defaults: {
				accept         : false,
				action         : "",
				autoUpload     : true,
				beforeSend     : function(formdata) { return formdata; },
				chunked        : false,
				chunkSize      : 100,
				customClass    : "",
				dataType       : "html",
				label          : "Drag and drop files or click to select",
				leave          : "You have uploads pending, are you sure you want to leave this page?",
				maxConcurrent  : 2,
				// maxQueue       : 2,
				maxFiles       : false,
				maxSize        : 5242880, // 5 mb
				multiple       : true,
				postData       : {},
				postKey        : "file",
				theme          : "fs-light"
			},

			classes: [
				"input",
				"target",
				"multiple",
				"dropping",
				"disabled",
				"focus"
			],

			methods: {
				_setup        : setup,
				_construct    : construct,
				_destruct     : destruct,

				disable       : disableUpload,
				enable        : enableUpload,
				abort         : abortUpload,
				start         : startUpload
			}
		}),

		// Localize References

		Classes       = Plugin.classes,
		RawClasses    = Classes.raw,
		Events        = Plugin.events,
		Functions     = Plugin.functions,

		Window        = Formstone.window,
		$Window       = Formstone.$window,

		// Internal
		BlobSliceMethod = false;

		/**
		 * @events
		 * @event chunkcomplete "File chunk complete"
		 * @event chunkstart "File chunk starting"
		 * @event chunkerror "File chunk error"
		 * @event complete "All uploads are complete"
		 * @event filecomplete "Specific upload complete"
		 * @event filedragenter "File dragged into target"
		 * @event filedragleave "File dragged from target"
		 * @event filedragover "File dragged over target"
		 * @event fileerror "Specific upload error"
		 * @event fileprogress "Specific upload progress"
		 * @event filestart "Specific upload starting"
		 * @event start "Uploads starting"
		 * @event queued "Files are queued for upload"
		 */

		Events.chunkComplete   = "chunkcomplete";
		Events.chunkError      = "chunkerror";
		Events.chunkStart      = "chunkstart";
		Events.complete        = "complete";
		Events.fileComplete    = "filecomplete";
		Events.fileDragEnter   = "filedragenter";
		Events.fileDragLeave   = "filedragleave";
		Events.fileDragOver    = "filedragover";
		Events.fileError       = "fileerror";
		Events.fileProgress    = "fileprogress";
		Events.fileStart       = "filestart";
		Events.start           = "start";
		Events.queued          = "queued";

})

);
