Ink.createModule(
	'Ink.UI.Upload',
	'1',
	[
	'Ink.Dom.Event_1',
	'Ink.Dom.Element_1',
	'Ink.Dom.Browser_1',
	'Ink.UI.Common_1'
],
	function(Event, Element, Browser, Common) {
	'use strict';

	var DirectoryReader = function(options) {
			this.init( options );
	};

	DirectoryReader.prototype = {
			init: function(options) {
				this._options = Ink.extendObj(
				{
				entry:      undefined,
				maxDepth:   10
				},
				options || {}
				);

				try {
					this._read();
				} catch (e) {
					Ink.error( e );
				}
				},

		_read: function() {
			if ( ! this._options.entry) {
				Ink.error( 'You must specify the entry!' );
				return;
			}

			try {
				this._readDirectories();
			} catch (e) {
				Ink.error( e );
			}
			},

		_readDirectories: function() {
			var entries  = [],
				running  = false,
				maxDepth = 0;

			/* TODO return as tree because much better well */
			var _readEntries = Ink.bind(
				function(currentEntry) {
				var dir     = currentEntry.createReader();
					running = true;

				dir.readEntries(
					Ink.bind(
					function(res) {
					if (res.length > 0) {
							for (var i = 0, len = res.length; i < len; i++) {
								entries.push( res[i] );
								if ( ! res[i].isDirectory) {
									continue;
								}
								maxDepth = this.clearArray( res[i].fullPath.split( '/' ) );
								maxDepth.shift();
								maxDepth = maxDepth.length;
								if (maxDepth <= this._options.maxDepth) {
									_readEntries( res[i] );
								}
								}
							if (this._stopActivityTimeout) {
								clearTimeout( this._stopActivityTimeout );
								}
							this._stopActivityTimeout = setTimeout(
							function() {
							running = false;
								},
							250
								);
					}
					if ( ! res.length) {
							running = false;
					}
				},
					this
					),
					Ink.bind(
					function(err) {
					this._options.readError( err, currentEntry );
				},
					this
					)
					);
			},
				this
				);

			_readEntries( this._options.entry );

			var activity;
			var checkActivity = function() {
				if (running) {
					return false;
				}
				clearInterval( activity );
				if (this._options.readComplete && typeof this._options.readComplete === 'function') {
					this._options.readComplete( entries );
				}
				return true;
			};

			activity = setInterval( Ink.bind( checkActivity, this ), 250 );
			},

		clearArray: function(arr) {
			for (var i = arr.length - 1; i >= 0; i--) {
				if (typeof(arr[i]) === 'undefined' || arr[i] === null || arr[i] === '') {
					arr.splice( i, 1 );
				}
			}
			return arr;
			}
	};

	var Queue = {
			lists:  [],
			items:  [],

			/**
			 * Create new queue list
			 *
			 * @function create
			 * @public
			 * @param {String} list name
			 * @param {Function} function to iterate on items
			 * @return {Object} list id
			 */
			create: function(name) {
				var id;
				name = String( name );
				this.lists.push( {name: name} );
				id = this.lists.length - 1;
				return id;
				},

		getItems: function(parentId) {
			if ( ! parentId) {
				return this.items;
			}
			var items = [];
			for (var i = 0, len = this.items.length; i < len; i++) {
				if (this.items[i].parentId === parentId) {
					items.push( this.items[i] );
				}
			}

			return items;
			},

		/**
		 * Delete list
		 *
		 * @function purge
		 * @public
		 * @param {String} List name
		 * @return {Object} removed list
		 */
		purge: function(id, keepList) {
			if (typeof(id) !== 'number' || isNaN( Number( id ) )) {
				return false;
			}
			try {
				for (var i = this.items.length; i >= 0; i--) {
					if (this.items[i] && id === this.items[i].parentId) {
						this.remove( this.items[i].parentId, this.items[i].pid );
					}
				}
				if ( ! keepList) {
					this.lists.splice( id, 1 );
				}
				return true;
			} catch (e) {
				Ink.error( 'Purge: invalid id' );
				return false;
			}
			},

		/**
		 * add an item to a list
		 *
		 * @function add
		 * @public
		 * @param {String} name
		 * @param {Object} item
		 * @return {Number} pid
		 */
		add: function(parentId, item, priority) {
			if ( ! this.lists[parentId]) {
				return false;
			}
			if (typeof(item) !== 'object') {
				item = String( item );
			}

			var pid  = parseInt( Math.round( Math.random() * 100000 ) + "" + Math.round( Math.random() * 100000 ), 10 );
			priority = priority || 0;

			this.items.push( {parentId: parentId, item: item, priority: priority || 0, pid: pid} );
			return pid;
			},

		/**
		 * View list
		 *
		 * @function view
		 * @public
		 * @param {Number} list id
		 * @param {Number} process id
		 * @return {Object} item
		 */
		view: function(parentId, pid) {
			var id = this._searchByPid( parentId, pid );
			if (id === false) {
				return false;
			}
			return this.items[id];
			},

		/**
		 * Remove an item
		 *
		 * @function remove
		 * @public
		 * @param {Object} item
		 * @return {Object|Boolean} removed item or false if not found
		 */
		remove: function(parentId, pid) {
			try {
				var id = this._searchByPid( parentId, pid );
				if (id === false) {
					return false;
				}
				this.items.splice( id, 1 );
				return true;
			} catch (e) {
				Ink.error( 'Remove: invalid id' );
				return false;
			}
			},

		_searchByPid: function(parentId, pid) {
			if ( ! parentId && typeof(parentId) === 'boolean' || ! pid) {
				return false;
			}

			parentId = parseInt( parentId, 10 );
			pid      = parseInt( pid, 10 );

			if (isNaN( parentId ) || isNaN( pid )) {
				return false;
			}

			for (var i = 0, len = this.items.length; i < len; i++) {
				if (this.items[i].parentId === parentId && this.items[i].pid === pid) {
					return i;
				}
			}
			return false;
			}
	};

	var UI = function(Upload) {
			this.Upload = Upload;
			this.init();
	};

	UI.prototype = {
			init: function() {
				this._fileButton = this.Upload._options.fileButton;
				this._dropzone   = this.Upload._options.dropzone;
				this._setDropEvent();
				this._setFileButton();
				},

		_setDropEvent: function() {
			var dropzones = this._dropzone;
			if ( ! dropzones) {
return; }

			for (var i = 0, len = dropzones.length; i < len; i++) {
				dropzones[i].ondrop      = Ink.bindEvent( this.Upload._dropEventHandler, this.Upload );
				dropzones[i].ondragleave = Ink.bindEvent( this._onDragLeave, this );
				dropzones[i].ondragend   = Ink.bindEvent( this._onDragEndEventHandler, this );
				dropzones[i].ondragdrop  = Ink.bindEvent( this._onDragEndEventHandler, this );
				dropzones[i].ondragenter = Ink.bindEvent( this._onDragEnterHandler, this );
				dropzones[i].ondragover  = Ink.bindEvent( this._onDragOverHandler, this );
			}
			},

		_onDragEnterHandler: function(ev) {
			if (ev && ev.stopPropagation) {
				ev.stopPropagation();
			}
			if (ev && ev.preventDefault) {
				ev.preventDefault();
			}
			if (ev) {
				ev.returnValue = false;
			}

			this.Upload.publish( 'DragEnter', ev );
			return false;
			},

		_onDragOverHandler: function(ev) {
			if ( ! ev) {
				return false;
			}
			ev.preventDefault();
			ev.stopPropagation();
			ev.returnValue = false;
			return true;
			},

		_onDragLeave: function(ev) {
			return this.Upload.publish( 'DragLeave', ev );
			},

		_onDragEndEventHandler: function(ev) {
			return this.Upload.publish( 'DragEnd', ev );
			},

		_setFileButton: function() {
			var btns = this._fileButton;
			if ( ! btns) {
return; }
			Event.observeMulti( btns, 'change', Ink.bindEvent( this._fileChangeHandler, this ) );
			},

		_fileChangeHandler: function(ev) {
			var btn   = Event.element( ev );
			var files = btn.files;
			var form  = Element.findUpwardsByTag( btn, 'form' );

			if ( ! files || ! window.FormData || ! ('withCredentials' in new XMLHttpRequest())) {
				form.parentNode.submit();
				return false;
			}
			this.Upload._addFilesToQueue( files );
			btn.value = "";
			}
	};

	var Upload = function(options) {
			this.Queue = Queue;
			this.init( options );
			this._events = {};
	};

	Upload.prototype = {
			// _events: {},

			/**
			 * This component is used to enable HTML5 upload on forms easily. It
			 * evens out differences between browsers which support HTML5 upload,
			 * and supports chunked uploads and directory tree uploads.
			 *
			 * Choose a drop zone and/or a file input. When the user drops the file
			 * on the drop zone element, or chooses it using the file input,
			 * Ink.UI.Upload takes care of uploading it through AJAX POST.
			 *
			 * The name given to the file in the POST request's data is chosen
			 * through the `fileFormName` option.
			 *
			 * On the server side, you will receive a POST with a Content-type of
			 * `multipart/form-data` or `x-www-form/urlencoded` if `useChunks`
			 * is `true`.
			 *
			 * @class Ink.UI.Upload_1
			 * @constructor
			 *
			 * @param options {Object} Options hash, containing:
			 * @param [options.dropzone] {Element} Element where the user can drop files onto.
			 * @param [options.fileButton] {Element} An `input[type="file"]` for the user to choose a file using a native dialog.
			 * @param [options.fileFormName='Ink_Filelist'] The name of the file in the POST request.
			 * @param [options.endpoint=window.location] The URL where we're POSTing the files to. Defaults to the current location, like a HTML form.
			 * @param [options.maxFileSize] Maximum file size in bytes. Defaults to 300mb.
			 * @param [INVALID_FILE_NAME] A regular expression to invalidate file names. For example, set this to `/\.png$/` if you don't want files with the ".png" extension. Remember that file extensions are just hints!
			 * @param [options.extraData] Add more data to your POST request. Each key in this hash gets added to the form data sent to the server.
			 * TODO chunk options, also write a bit above about chunking and the serverside of chunking.
			 * TODO directory options, also write a bit above about directories and the server end of directories.
			 */
			init: function(options) {
				if (typeof options === 'string') {
					options = Element.data( Common.elOrSelector( options, '1st argument' ) );
				}
				this._options = Ink.extendObj(
				{
				dropzone:           undefined,
				fileButton:         undefined,
				fileFormName:       'Ink_Filelist',  // TODO default to fileButton's [name] if available.
				endpoint:           '',
				maxFilesize:        300 << 20, // 300mb
				INVALID_FILE_NAME:  undefined,
				extraData:          {},
				// Chunks
				useChunks:          false,
				chunkSize:          4194304,  // 4MB
				minSizeToUseChunks: 20971520, // 20mb
				endpointChunk:      '',  // Where to send chunk data.
				endpointChunkCommit:'',  // Where to send the "chunk transaction" commit.
				// Directory trees
				foldersEnabled:     false,
				directoryMaxDepth:  10
				},
				options || {}
				);

				this._queueId      = Queue.create( 'Ink_UPLOAD' );
				this._queueRunning = false;
				this._folders      = {};

				if (this._options.dropzone) {
					this._options.dropzone =
					Common.elsOrSelector( this._options.dropzone, 'Ink.UI.Upload - dropzone' );
				}

				if (this._options.fileButton) {
					this._options.fileButton =
					Common.elsOrSelector( this._options.fileButton, 'Ink.UI.Upload - fileButton' );
				}

				if ( ! this._options.dropzone && ! this._options.fileButton) {
					throw new TypeError(
					'Ink.UI.Upload: Specify a fileButton or a Dropzone!'
						);
				}

				new UI( this );
				},

		_supportChunks: function(size) {
			return this._options.useChunks &&
					'Blob' in window &&
					(new Blob()).slice &&
					size > this._options.minSizeToUseChunks;
			},

		_dropEventHandler: function(ev) {
			Event.stopDefault( ev );

			this.publish( 'DropComplete', ev.dataTransfer );

			var data = ev.dataTransfer;

			if ( ! data || ! data.files || ! data.files.length) {
				return false;
			}

			this._files = data.files;
			this._files = Array.prototype.slice.call( this._files || [], 0 );

			// check if webkitGetAsEntry exists on first item
			if (data.items && data.items[0] && data.items[0].webkitGetAsEntry) {
				if ( ! this._options.foldersEnabled) {
					return setTimeout( Ink.bind( this._addFilesToQueue, this, this._files ), 0 );
				}
				var entry, folders = [];
				for (var i = ev.dataTransfer.items.length - 1; i >= 0; i--) {
					entry = ev.dataTransfer.items[i].webkitGetAsEntry();
					if (entry && entry.isDirectory) {
						folders.push( entry );
						this._files[i].isDirectory = true;
						this._files.splice( i, 1 );
					}
				}
				// starting callback hell
				this._addFolderToQueue(
					folders,
					Ink.bind(
					function() {
					setTimeout( Ink.bind( this._addFilesToQueue, this, this._files ), 0 );
				},
					this
					)
					);
			} else {
				setTimeout( Ink.bind( this._addFilesToQueue, this, this._files ), 0 );
			}

			return true;
			},

		_addFolderToQueue: function(folders, cb) {
			var files = [], invalidFolders = {};

			if ( ! folders || ! folders.length) {
				cb();
				return files;
			}

			var getFiles = function(entries) {
				var files = [];
				for (var i = 0, len = entries.length; i < len; i++) {
					if (entries[i].isFile) {
						files.push( entries[i] );
					}
				}
				return files;
			};

			var convertToFile = function(cb, index) {
				var fullPath;
				index = index || 0;
				if ( ! this._files[index]) {
					cb();
					return files;
				}
				if (this._files[index].constructor.name.toLowerCase() !== 'fileentry') {
					return convertToFile.apply( this, [cb, ++index] );
				}
				this._files[index].file(
					Ink.bind(
					function(res) {
					fullPath                     = this._files[index].fullPath; // bug
					this._files[index]           = res;
					this._files[index].hasParent = true;

					// if browser don't have it natively, set it
					if ( ! this._files[index].fullPath) {
							this._files[index].fullPath = fullPath;
					}
					convertToFile.apply( this, [cb, ++index] );
				},
					this
					),
					Ink.bind(
					function() {
					this._files.splice( index, 1 );
					convertToFile.apply( this, [cb, index] );
				},
					this
					)
					);
			};

			var getSubDirs = Ink.bind(
				function(index) {
				if ( ! folders[index]) {
						this._files = this._files.concat( files );
						convertToFile.call( this, cb );
						return false;
				}

				new DirectoryReader(
					{
					entry:      folders[index],
					maxDepth:   this._options.directoryMaxDepth,
					readComplete: Ink.bind(
						function(entries) {
						files = files.concat( getFiles( entries ) );
						// adding root dirs
						if ( ! folders[index] || folders[index].fullPath in this._folders) {
								return;
						}

						this._folders[folders[index].fullPath] = {
								items:      entries,
								files:      files,
								length:     entries.length,
								created:    false,
								root:       true
						};

						// adding sub dirs
						for (var i = 0, len = entries.length; i < len; i++) {
								if (entries[i].isFile) {
									continue;
									}
								if (entries[i].fullPath in invalidFolders) {
									delete invalidFolders[entries[i].fullPath];
									continue;
									}
								this._folders[entries[i].fullPath] = {
									created:    false,
									root:       false
									};
						}
						getSubDirs( ++index );
					},
						this
						),
					readError: Ink.bind(
						function(err, dir) {
					invalidFolders[dir.fullPath]       = {};
					invalidFolders[dir.fullPath].error = err;
					},
						this
						)
				}
					);
			},
				this
				);

			getSubDirs( 0 );
			return files;
			},

		_addFilesToQueue: function(files) {
			var file, fileID, o;
			for (var i = 0, len = files.length; i < len; i++) {
				file = files[i];

				if ( ! file.isDirectory) {
					// dirty hack to allow 0B files avoiding folders on GECKO
					if (file === null || ( ! file.type && file.size % 4096 === 0 && ( ! Browser.CHROME || ! this._options.foldersEnabled))) {
						this.publish( 'InvalidFile', file, 'size' );
						continue;
					}
				}

				if (file.size > this._options.maxFilesize) {
					this.publish( 'MaxSizeFailure', file, this._options.maxFilesize );
					continue;
				}

				fileID = parseInt( Math.round( Math.random() * 100000 ) + "" + Math.round( Math.random() * 100000 ), 10 );
				o      = { id: i, data: file, fileID: fileID, directory: file.isDirectory };
				Queue.add( this._queueId, o );

				this.publish( 'FileAddedToQueue', o );
			}
			this._processQueue( true );
			this._files = [];
			},

		_processQueue: function(internalUpload) {
			if (this._queueRunning) {
				return false;
			}

			this.running       = 0;
			var max            = 1, i = 0, items,
				queueLen       = Queue.items.length;
			this._queueRunning = true;

			this.interval = setInterval(
				Ink.bind(
				function() {
				if (Queue.items.length === i && this.running === 0) {
						Queue.purge( this._queueId, true );
						this._queueRunning = false;
						clearInterval( this.interval );
						this.publish( 'QueueEnd', this._queueId, queueLen );
				}

				items = Queue.getItems( this._queueId );

				if (this.running < max && items[i]) {
						if ( ! items[i].canceled) {
							_doRequest.call( this, items[i].pid, items[i].item.data, items[i].item.fileID, items[i].item.directory, internalUpload );
							this.running++;
							i++;
							} else {
							var j = i;
							while (items[j] && items[j].canceled) {
								i++;
								j++;
							}
							}
						return true;
				}
				return false;
			},
				this
				),
				100
				);

			var _doRequest = function(pid, data, fileID, directory, internalUpload) {
				var o = {
					file:   data,
					fileID: fileID,
					cb: Ink.bind(
						function() {
						this.running--;
					},
						this
						)
				};
				if (internalUpload) {
					if (directory) {
						// do magic
						o.cb();
					} else {
						this._upload( o );
					}
				}
			};

			return true;
			},

		_upload: function(o) {
			var file   = o.file,
				xhr    = new XMLHttpRequest(),
				fileID = o.fileID;

			this.publish( 'BeforeUpload', file, this._options.extraData, fileID, xhr, this._supportChunks( file.size ) );

			var forceAbort = function(showError) {
				if (o.cb && typeof(o.cb === 'function')) {
					o.cb();
				}

				this.publish(
					'OnProgress',
					{
					length: file.size,
					lengthComputable: true,
					loaded: file.size,
					total: file.size
				},
					file,
					fileID
					);
				this.publish( 'EndUpload', file, fileID, (showError ? { error : true } : true) );
				this.publish( 'InvalidFile', file, 'name' );
				xhr.abort();
			};

			if (this._options.INVALID_FILE_NAME && this._options.INVALID_FILE_NAME instanceof RegExp) {
				if (this._options.INVALID_FILE_NAME.test( o.file.name )) {
					forceAbort.call( this );
					return;
				}
			}

			// If file was renamed, abort it
			// FU OPERA: Opera always return lastModified date as null
			if ( ! file.lastModifiedDate && ! Ink.Dom.Browser.OPERA) {
				forceAbort.call( this, true );
				return;
			}

			xhr.upload.onprogress = Ink.bind( this.publish, this, 'OnProgress', file, fileID );

			var endpoint, method;
			if (this._supportChunks( file.size )) {
				if (file.size <= file.chunk_offset) {
					endpoint = this._options.endpointChunkCommit;
					method   = 'POST';
				} else {
					endpoint = this._options.endpointChunk;
					if (file.chunk_upload_id) {
						endpoint += '?upload_id=' + file.chunk_upload_id;
					}
					if (file.chunk_offset) {
						endpoint += '&offset=' + file.chunk_offset;
					}
					method = 'PUT';
				}
			} else {
				endpoint = this._options.endpoint;
				method   = 'POST';
			}

			xhr.open( method, endpoint, true );
			xhr.withCredentials = true;
			xhr.setRequestHeader( "x-requested-with", "XMLHttpRequest" );
			if (this._supportChunks( file.size )) {
				xhr.setRequestHeader( 'Content-type', 'application/x-www-form-urlencoded' );
			}

			var fd = new FormData(),
				blob;

			if ("Blob" in window && typeof Blob === 'function') {
				blob = new Blob( [file], { type: file.type } );
				if (this._supportChunks( file.size )) {
					file.chunk_offset = file.chunk_offset || 0;
					blob              = blob.slice( file.chunk_offset, file.chunk_offset + this._options.chunkSize );
				} else {
					fd.append( this._options.fileFormName, blob, file.name );
				}
			} else {
				fd.append( this._options.fileFormName, file );
			}

			if ( ! this._supportChunks( file.size )) {
				for (var k in this._options.extraData) {
					if (this._options.extraData.hasOwnProperty( k )) {
						fd.append( k, this._options.extraData[k] );
					}
				}
			} else {
				fd.append( 'upload_id', file.chunk_upload_id );
				fd.append( 'path', file.upload_path );
			}

			if ( ! file.hasParent) {
				if ( ! this._supportChunks( file.size )) {
					xhr.send( fd );
				} else {
					if (file.size <= file.chunk_offset) {
						xhr.send( 'upload_id=' + file.chunk_upload_id + '&path=' + file.upload_path + '/' + file.name );
					} else {
						xhr.send( blob );
					}
				}
			} else {
				this.publish(
					'cbCreateFolder',
					file.parentID,
					file.fullPath,
					this._options.extraData,
					this._folders,
					file.rootPath,
					Ink.bind(
					function() {
					if ( ! this._supportChunks( file.size )) {
							xhr.send( fd );
					} else {
							if (file.size <= file.chunk_offset) {
								xhr.send( 'upload_id=' + file.chunk_upload_id + '&path=' + file.upload_path + '/' + file.name );
								} else {
								xhr.send( blob );
								}
					}
				},
					this
					)
					);
			}

			xhr.onload = Ink.bindEvent(
				function() {
				/* jshint boss:true */
				if (this._supportChunks( file.size ) && file.size > file.chunk_offset) {
						if (xhr.response) {
							var response = JSON.parse( xhr.response );

							// check expected offset
							var invalidOffset = file.chunk_offset && response.offset !== (file.chunk_offset + this._options.chunkSize) && file.size !== response.offset;
							if (invalidOffset) {
								if (o.cb) {
									o.cb();
								}
								this.publish( 'ErrorUpload', file, fileID );
							} else {
								file.chunk_upload_id = response.upload_id;
								file.chunk_offset    = response.offset;
								file.chunk_expires   = response.expires;
								this._upload( o );
							}
							} else {
							if (o.cb) {
								o.cb();
							}
							this.publish( 'ErrorUpload', file, fileID );
							}
						return (xhr = null);
				}

				if (o.cb) {
						o.cb();
				}

				if (xhr.responseText && xhr['status'] < 400) {
						this.publish( 'EndUpload', file, fileID, xhr.responseText );
				} else {
						this.publish( 'ErrorUpload', file, fileID );
				}
				return (xhr = null);
			},
				this
				);

			xhr.onerror = Ink.bindEvent(
				function() {
				if (o.cb) {
						o.cb();
				}
				this.publish( 'ErrorUpload', file, fileID );
			},
				this
				);

			xhr.onabort = Ink.bindEvent(
				function() {
				if (o.cb) {
						o.cb();
				}
				this.publish(
					'AbortUpload',
					file,
					fileID,
					{
					abortAll: Ink.bind( this.abortAll, this ),
					abortOne: Ink.bind( this.abortOne, this )
				}
					);
			},
				this
				);
			},

		abortAll: function() {
			if ( ! this._queueRunning) {
				return false;
			}
			clearInterval( this.interval );
			this._queueRunning = false;
			Queue.purge( this._queueId, true );
			return true;
			},

		abortOne: function(id, cb) {
			var items = Queue.getItems( 0 ),
				o;
			for (var i = 0, len = items.length; i < len; i++) {
				if (items[i].item.fileID === id) {
					o = {
						id:         items[i].item.fileID,
						name:       items[i].item.data.name,
						size:       items[i].item.data.size,
						hasParent:  items[i].item.data.hasParent
					};
					Queue.remove( 0, items[i].pid );
					if (cb) {
						cb( o );
					}
					return true;
				}
			}
			return false;
			},

		subscribe: function(eventName, fn) {
			if ( ! this._events[eventName]) {
				this._events[eventName] = [];
			}
			this._events[eventName].push( fn );
			return this._events[eventName];
			},

		publish: function(eventName) {
			var events = this._events[eventName],
				args   = Array.prototype.slice.call( arguments || [], 0 );

			if ( ! events) {
				return;
			}

			for (var i = 0, len = events.length; i < len; i++) {
				try {
					events[i].apply( this, args.splice( 1, args.length ) );
				} catch (err) {
					Ink.error( eventName + ": " + err );
				}
			}
			}
	};

	return Upload;
}
	);
