/** Fileuploader
 *
 * Usage: jQuery(element).FileUploader(options);
 *
 * Options:
 *         name                - parameter name to send file with
 *                               (technical note: this uses 'raw_upload_param_name' parameter understandable for raw_upload middleware)
 *         url                 - target url for the file upload;
 *         data                - additional parameters for upload url
 *         dropbox             - jQuery selector that denotes area to drop files instead of opening them;
 *                               when not set, dropbox is created as big area on the file selector;
 *         uploadEvent         - name of the event that triggers uploading file list;
 *                               when not set (by default), upload is triggered immediately after closing file dialog;
 *         deactivateOnUpload  - disable a possibility of selection new file while upload previous selection (default: true);
 *                               works only in triggered mode of upload; (TODO) enable it in automatic upload mode;
 *         debug               - debug mode (default: false)
 *         multiple            - multiple file upload possibility (default: true)
 *         allowedExtensions   - array with all allowed extension (default: [] <=> all extensions)
 *         sizeLimit           - maximal size of the file (default: 0 <=> no limit)
 *         minSizeLimit        - minimal size of the file (default: 0 <=> no limit)
 *         with                - (TODO) additional parameters (as in dialog)
 *
 *
 * Callbacks:
 *         onSubmit        - function(id, fileName)
 *         onProgress      - function(id, fileName, loaded, total)
 *         onComplete      - function(id, fileName, response)
 *         onCancel        - function(id, fileName, fileCount)
 *         onFilesSelected - function{fileCount)
 *         onListSubmit    - function(fileCount)
 *         onListComplete  - function(lastResponse)
 *
 * Events:
 *         [uploadEvent]      - bound only on demand; when triggered causes an upload of file list;
 *         disable            - disable file input selector so no more files can be selected;
 *         enable             - enable file input selector;
 *
 *         filesSelected      - triggered when user selects a set of files; arguments: event, fileCount
 *         uploadStarted      - triggered when upload started for particular file; arguments: event, id, fileName
 *         uploadFinished     - triggered when upload finished for particular file; arguments: event, id, fileName, response
 *         uploadListStarted  - triggered when upload of a list of files is started; arguments: event, fileCount
 *         uploadListFinished - triggered when upload of a list of files is finished; arguments: event, lastResponse
 *
 **/

(function($) {

	$.fn.FileUploader = function(options) {

		FileUploaderBasic = function(o) {
			this._options = {
				// set to true to see the server response
				name: 'file',
				uploadEvent: null,
				debug: false,
				action: '/server/upload',
				data: {},
				button: null,
				multiple: true,
				maxConnections: 3,
				// validation
				allowedExtensions: [],
				sizeLimit: 0,
				minSizeLimit: 0,
				disableOnUpload: true,
				// events
				// return false to cancel submit
				onSubmit: function(id, fileName) {
				},
				onProgress: function(id, fileName, loaded, total) {
				},
				onComplete: function(id, fileName, response) {
				},
				onCancel: function(id, fileName, fileCount) {
				},
				onFilesSelected: function(fileCount) {
				},
				onListSubmit: function(fileCount) {
				},
				onListComplete: function(lastResponse) {
				},
				// messages
				messages: {
					typeError: _("{file} has invalid extension. Only {extensions} are allowed."),
					sizeError: _("{file} is too large, maximum file size is {sizeLimit}."),
					minSizeError: _("{file} is too small, minimum file size is {minSizeLimit}."),
					emptyError: _("{file} is empty, please select files again without it."),
					onLeave: _("The files are being uploaded, if you leave now the upload will be cancelled.")
				},
				showMessage: function(message) {
					alert(message);
				}
			};
			$.extend(this._options, o);

			// number of files being uploaded
			this._filesInProgress = 0;
			this._handler = this._createUploadHandler();

			if (this._options.button) {
				this._button = this._createUploadButton(this._options.button);
			}

			this._preventLeaveInProgress();
		};

		FileUploaderBasic.prototype = {
			setParams: function(data) {
				this._options.data = data;
			},
			getInProgress: function() {
				return this._filesInProgress;
			},
			_createUploadButton: function(element) {
				var self = this;

				return new UploadButton({
					element: element,
					multiple: this._options.multiple && UploadHandlerXhr.isSupported(),
					onChange: function(input) {
						self._onInputChange(input);
					}
				});
			},
			_createUploadHandler: function() {
				var self = this,
						handlerClass;

				if (UploadHandlerXhr.isSupported()) {
					handlerClass = 'UploadHandlerXhr';
				} else {
					handlerClass = 'UploadHandlerForm';
				}

				var handler = new window[handlerClass]({
					name: this._options.name,
					debug: this._options.debug,
					action: this._options.action,
					maxConnections: this._options.maxConnections,
					element: this._options.element,
					onProgress: function(id, fileName, loaded, total) {
						self._onProgress(id, fileName, loaded, total);
						self._options.onProgress(id, fileName, loaded, total);
					},
					onComplete: function(id, fileName, result) {
						self._onComplete(id, fileName, result);
						self._options.onComplete(id, fileName, result);
					},
					onCancel: function(id, fileName, fileCount) {
						self._onCancel(id, fileName, fileCount);
						self._options.onCancel(id, fileName, fileCount);
					},
					onListComplete: function(lastResponse) {
						self._options.onListComplete(lastResponse);
					}
				});

				return handler;
			},
			_preventLeaveInProgress: function() {
				var self = this;

				$(window).bind('beforeunload', function(e) {
					if (!self._filesInProgress) {
						return;
					}

					var e = e || window.event;
					// for ie, ff
					e.returnValue = self._options.messages.onLeave;
					// for webkit
					return self._options.messages.onLeave;
				});
			},
			_onSubmit: function(id, fileName) {
				this._filesInProgress++;
			},
			_onProgress: function(id, fileName, loaded, total) {
			},
			_onComplete: function(id, fileName, result) {
				this._filesInProgress--;
				if (result.error) {
					this._options.showMessage(result.error);
				}
			},
			_onCancel: function(id, fileName, fileCount) {
				this._filesInProgress--;
			},
			_onInputChange: function(input) {
				var input = this._button._input;

				this._options.onFilesSelected(this._handler.fileCount());
				$(this._options.element).trigger('filesSelected', [this._handler.fileCount()]);

				if (this._handler.isXhrHandler) {
					var files = input.get(0).files;
					this._uploadFileList(files);
					if (!this._options.uploadEvent) {
						this._options.onListSubmit(files.length);
						$(this._options.element).trigger('uploadListStarted', [files.length]);
					}
				} else {
					if (this._validateFile(input)) {
						this._uploadFile(input);
						if (!this._options.uploadEvent) {
							this._options.onListSubmit(1);
							$(this._options.element).trigger('uploadListStarted', [1]);
						}
					}
				}
				this._button.reset();
			},
			_uploadFileList: function(files) {
				for (var i = 0; i < files.length; i++) {
					if (!this._validateFile(files[i])) {
						return;
					}
				}

				for (var i = 0; i < files.length; i++) {
					this._uploadFile(files[i]);
				}
			},
			_uploadFile: function(fileContainer) {
				var id = this._handler.add(fileContainer);
				if (!this._ids) this._ids = [];
				this._ids.push(id);
				var fileName = this._handler.getName(id);

				if (this._options.onSubmit(id, fileName) !== false) {
					this._onSubmit(id, fileName);
					if (!this._options.uploadEvent) {
						this._handler.upload(id, this._options.data);
					}
				}
			},
			_uploadAllFiles: function() {
				if (this._ids) {
					this._options.onListSubmit(id, this._ids.length);
					$(this._options.element).trigger('uploadListStarted', [this._ids.length]);
					for (var i = 0; i < this._ids.length; i++) {
						var id = this._ids[i];
						this._handler.upload(id, this._options.data);
					}
				}
				this._ids = [];
			},
			_validateFile: function(file) {
				var name, size;
				if (file.val) {
					// it is a file input
					// get input value and remove path to normalize
					name = file.val().replace(/.*(\/|\\)/, "");
				} else {
					// fix missing properties in Safari
					name = file.fileName != null ? file.fileName : file.name;
					size = file.fileSize != null ? file.fileSize : file.size;
				}

				if (! this._isAllowedExtension(name)) {
					this._error('typeError', name);
					return false;

				} else if (size === 0) {
					this._error('emptyError', name);
					return false;

				} else if (size && this._options.sizeLimit && size > this._options.sizeLimit) {
					this._error('sizeError', name);
					return false;

				} else if (size && size < this._options.minSizeLimit) {
					this._error('minSizeError', name);
					return false;
				}

				return true;
			},
			_error: function(code, fileName) {
				var message = this._options.messages[code];

				function r(name, replacement) {
					message = message.replace(name, replacement);
				}

				r('{file}', this._formatFileName(fileName));
				r('{extensions}', this._options.allowedExtensions.join(', '));
				r('{sizeLimit}', this._formatSize(this._options.sizeLimit));
				r('{minSizeLimit}', this._formatSize(this._options.minSizeLimit));

				this._options.showMessage(message);
			},
			_formatFileName: function(name) {
				if (name.length > 33) {
					name = name.slice(0, 19) + '...' + name.slice(-13);
				}
				return name;
			},
			_isAllowedExtension: function(fileName) {
				var ext = (-1 !== fileName.indexOf('.')) ? fileName.replace(/.*[.]/, '').toLowerCase() : '';
				var allowed = this._options.allowedExtensions;

				if (!allowed.length) {
					return true;
				}

				for (var i = 0; i < allowed.length; i++) {
					if (allowed[i].toLowerCase() == ext) {
						return true;
					}
				}

				return false;
			},
			_formatSize: function(bytes) {
				var i = -1;
				do {
					bytes = bytes / 1024;
					i++;
				} while (bytes > 99);

				return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i];
			},
			_disableFileInput: function() {
				$('input', this._element).attr("disabled", "disabled");
			},
			_enableFileInput: function() {
				$('input', this._element).removeAttr("disabled");
			}
		};


		/**
		 * Class that creates upload widget with drag-and-drop and file list
		 * @inherits FileUploaderBasic
		 */
		FileUploader = function(o) {
			// call parent constructor
			FileUploaderBasic.apply(this, arguments);

			// additional options
			$.extend(this._options, {
				element: null,
				// if set, will be used instead of qq-upload-list in template
				listElement: null,

				promptText: _('Upload a file'),
				dropPromptText: "<span>" + _('Drop files here to upload') + "</span>",
				cancelText: _('Cancel'),
				failedText: _('Failed'),

				template: '<div class="FileUploader">' +
									'<div class="FileUploaderDropArea"></div>' +
									'<div class="FileUploaderButton"></div>' +
									'<ul class="FileUploaderList"></ul>' +
									'</div>',

				// template for one item in file list
				fileTemplate: '<li>' +
											'<span class="FileUploaderFile"></span>' +
											'<span class="FileUploaderSpinner"></span>' +
											'<span class="FileUploaderSize"></span>' +
											'<a class="FileUploaderCancel" href="#">Cancel</a>' +
											'<span class="FileUploaderFailedText">Failed</span>' +
											'</li>',

				classes: {
					// used to get elements from templates
					button: 'FileUploaderButton',
					drop: 'FileUploaderDropArea',
					dropActive: 'FileUploaderDropAreaActive',
					list: 'FileUploaderList',

					file: 'FileUploaderFile',
					spinner: 'FileUploaderSpinner',
					size: 'FileUploaderSize',
					cancel: 'FileUploaderCancel',

					// added to list item when upload completes
					// used in css to hide progress spinner
					success: 'FileUploaderSuccess',
					fail: 'FileUploaderFail',
					failedText: 'FileUploaderFailedText'
				}
			});
			// overwrite options with user supplied
			$.extend(this._options, o);

			var element = $(this._options.element);
			if (element.is("div")) {
				this._element = this._options.element;
				this._element.innerHTML = this._options.template;
				this._find(this._element, 'button').html(this._options.promptText);
			}
			else {
				this._element = this._options.element.parentNode;
				element.replaceWith(this._options.template);
				this._find(this._element, 'button').html(element);
			}
			this._find(this._element, 'drop').html(this._options.dropPromptText);
			this._find(this._element, 'cancel').html(this._options.cancelText);
			this._find(this._element, 'fail').html(this._options.failedText);


			this._listElement = this._options.listElement || this._find(this._element, 'list');

			this._classes = this._options.classes;

			this._button = this._createUploadButton(this._find(this._element, 'button'));

			this._bindEvents();
			this._bindCancelEvent();
			this._setupDragDrop();
		};

		// inherit from Basic Uploader
		$.extend(FileUploader.prototype, FileUploaderBasic.prototype);

		$.extend(FileUploader.prototype, {
			// Gets one of the elements listed in this._options.classes
			_find: function(parent, type) {
				return $('.' + this._options.classes[type], $j(parent));
			},
			_setupDragDrop: function() {
				var self = this,
						dropArea = this._options.dropbox || this._find(this._element, 'drop');

				var dz = new UploadDropZone({
					element: dropArea,
					onEnter: function(e) {
						if (!self._options.dropbox) dropArea.addClass(self._classes.dropActive);
						e.stopPropagation();
					},
					onLeave: function(e) {
						e.stopPropagation();
					},
					onLeaveNotDescendants: function(e) {
						if (!self._options.dropbox) dropArea.removeClass(self._classes.dropActive);
					},
					onDrop: function(e) {
						if (!self._options.dropbox) {
							dropArea.hide();
							dropArea.removeClass(self._classes.dropActive);
						}
						self._options.onFilesSelected(self._handler.fileCount());
						$(self._options.element).trigger('filesSelected', [self._handler.fileCount()]);
						self._uploadFileList(e.dataTransfer.files);
						self._find($('body'), 'drop').hide();
					}
				});

				if (!this._options.dropbox) dropArea.hide();

				$(document).bind('dragenter', function(e) {
					if (!dz._isValidFileDrag(e.originalEvent) || self._options.dropbox) return;
					dropArea.show();
				});

				$(document).bind('dragleave', function(e) {
					e = e.originalEvent;
					if (!dz._isValidFileDrag(e)) return;
					var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
					// only fire when leaving document out
					if (! relatedTarget || relatedTarget.nodeName == "HTML") {
						if (!self._options.dropbox) dropArea.hide();
					}
				});
			},
			_onSubmit: function(id, fileName) {
				FileUploaderBasic.prototype._onSubmit.apply(this, arguments);
				this._addToList(id, fileName);
			},
			_onProgress: function(id, fileName, loaded, total) {
				FileUploaderBasic.prototype._onProgress.apply(this, arguments);

				var item = this._getItemByFileId(id);
				var size = this._find(item, 'size');
				size.show();

				var text;
				if (loaded != total) {
					text = Math.round(loaded / total * 100) + '% from ' + this._formatSize(total);
				} else {
					text = this._formatSize(total);
				}

				size.text(text);
			},
			_onComplete: function(id, fileName, result) {
				FileUploaderBasic.prototype._onComplete.apply(this, arguments);

				// mark completed
				var item = this._getItemByFileId(id);
				this._find(item, 'cancel').remove();
				this._find(item, 'spinner').remove();

				if (!result.fail) {
					item.addClass(this._classes.success);
				} else {
					item.addClass(this._classes.fail);
				}
			},
			_addToList: function(id, fileName) {
				var item = $(this._options.fileTemplate);
				item.attr('data-FileId', id);

				var fileElement = this._find(item, 'file');
				fileElement.text(this._formatFileName(fileName));
				this._find(item, 'size').hide();
				this._listElement.append(item);
			},
			_getItemByFileId: function(id) {
				var item = $j('li[data-FileId=' + id + ']', this._listElement)
				return item;
			},
			_bindEvents: function() {
				var element = $(this._element);
				var upload_event_name = this._options.uploadEvent;
				if (upload_event_name) {
					element.bind(upload_event_name, $.proxy(this._uploadAllFiles, this));
				}
				element.bind('disable', $.proxy(this._disableFileInput, this))
				element.bind('enable', $.proxy(this._enableFileInput, this))

				if (this._options.deactivateOnUpload) {
					element.bind('uploadListStarted', function() { element.trigger('disable'); });
					element.bind('uploadListFinished', function() { element.trigger('enable'); });
				}
			},
			/**
			 * delegate click event for cancel link
			 **/
			_bindCancelEvent: function() {
				var self = this,
						list = this._listElement;

				list.click(function(event) {
					event = event || window.event;
					var target = event.target || event.srcElement;
					target = $(target);

					if (target.hasClass(self._classes.cancel)) {
						event.preventDefault();

						var item = target.parent();
						self._handler.cancel(item.attr('data-FileId'));
						item.remove();
					}
				});
			}
		});

		UploadDropZone = function(o) {
			this._options = {
				element: null,
				onEnter: function(e) {
				},
				onLeave: function(e) {
				},
				// is not fired when leaving element by hovering descendants
				onLeaveNotDescendants: function(e) {
				},
				onDrop: function(e) {
				}
			};
			$.extend(this._options, o);

			this._element = this._options.element;

			this._disableDropOutside();
			this._bindEvents();
		};

		UploadDropZone.prototype = {
			_disableDropOutside: function(e) {
				// run only once for all instances
				if (!UploadDropZone.dropOutsideDisabled) {

					$('document').bind('dragover', function(e) {
						e = e.originalEvent;
						if (e.dataTransfer) {
							e.dataTransfer.dropEffect = 'none';
							e.preventDefault();
						}
					});

					UploadDropZone.dropOutsideDisabled = true;
				}
			},
			_bindEvents: function() {
				var self = this;

				self._element.bind('dragover', function(e) {
					e = e.originalEvent;
					if (!self._isValidFileDrag(e)) return;

					var effect = e.dataTransfer.effectAllowed;
					if (effect == 'move' || effect == 'linkMove') {
						e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed)
					} else {
						e.dataTransfer.dropEffect = 'copy'; // for Chrome
					}

					e.stopPropagation();
					e.preventDefault();
				});

				self._element.bind('dragenter', function(e) {
					e = e.originalEvent;
					if (!self._isValidFileDrag(e)) return;
					self._options.onEnter(e);
				});

				self._element.bind('dragleave', function(e) {
					e = e.originalEvent;
					if (!self._isValidFileDrag(e)) return;
					self._options.onLeave(e);

					var relatedTarget = document.elementFromPoint(e.clientX, e.clientY);
					// do not fire when moving a mouse over a descendant
					if ($.contains(this, relatedTarget)) return;

					self._options.onLeaveNotDescendants(e);
				});

				self._element.bind('drop', function(e) {
					e = e.originalEvent;
					if (!self._isValidFileDrag(e)) return;
					e.preventDefault();
					self._options.onDrop(e);
				});
			},
			_isValidFileDrag: function(e) {
				var dt = e.dataTransfer,
					// do not check dt.types.contains in webkit, because it crashes safari 4
						isWebkit = navigator.userAgent.indexOf("AppleWebKit") > -1;

				// dt.effectAllowed is none in Safari 5
				// dt.types.contains check is for firefox
				return dt && dt.effectAllowed != 'none' &&
							 (dt.files || (!isWebkit && dt.types.contains && dt.types.contains('Files')));

			}
		};

		UploadButton = function(o) {
			this._options = {
				element: null,
				// if set to true adds multiple attribute to file input
				multiple: false,
				// name attribute of file input
				name: 'file',
				onChange: function(input) {
				},
				hoverClass: 'FileUploaderButtonHover',
				focusClass: 'FileUploaderButtonFocus'
			};

			$.extend(this._options, o);

			this._element = this._options.element;

			// make button suitable container for input
			this._element.css({
				position: 'relative',
				overflow: 'hidden',
				// Make sure browse button is in the right side
				// in Internet Explorer
				direction: 'ltr'
			});

			this._input = this._createInput();
		};

		UploadButton.prototype = {
			/* returns file input element */
			getInput: function() {
				return this._input;
			},
			/* cleans/recreates the file input */
			reset: function() {
				var disabled = this._input.attr('disabled');
				this._input.remove();
				this._element.removeClass(this._options.focusClass);
				this._input = this._createInput({disabled: disabled});
			},
			_createInput: function(attrs) {
				var input = $j("<input />");

				if (this._options.multiple) {
					input.attr("multiple", "multiple");
				}

				input.attr("type", "file");
				input.attr("name", this._options.name);
				input.attr(attrs);

				input.css({
					position: 'absolute',
					// in Opera only 'browse' button
					// is clickable and it is located at
					// the right side of the input
					right: 0,
					top: 0,
					fontFamily: 'Arial',
					// 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
					fontSize: '118px',
					margin: 0,
					padding: 0,
					cursor: 'pointer',
					opacity: 0
				});

				this._element.append(input);

				var self = this;
				input.change(function() {
					self._options.onChange(input);
				});

				input.mouseover(function() {
					self._element.addClass(self._options.hoverClass);
				});
				input.mouseout(function() {
					self._element.removeClass(self._options.hoverClass);
				});
				input.focus(function() {
					self._element.addClass(self._options.focusClass);
				});
				input.blur(function() {
					self._element.removeClass(self._options.focusClass);
				});

				// IE and Opera, unfortunately have 2 tab stops on file input
				// which is unacceptable in our case, disable keyboard access
				if (window.attachEvent) {
					// it is IE or Opera
					input.attr('tabIndex', "-1");
				}

				return input;
			}
		};

		/**
		 * Class for uploading files, uploading itself is handled by child classes
		 */
		UploadHandlerAbstract = function(o) {
			this._options = {
				debug: false,
				action: '/upload',
				// maximum number of concurrent uploads
				maxConnections: 999,
				onProgress: function(id, fileName, loaded, total) {
				},
				onComplete: function(id, fileName, response) {
				},
				onCancel: function(id, fileName, fileCount) {
				}
			};
			$.extend(this._options, o);

			this._queue = [];
			// params for files in queue
			this._data = [];
		};
		UploadHandlerAbstract.prototype = {
			log: function(str) {
				if (this._options.debug && window.console) console.log('[uploader] ' + str);
			},
			/**
			 * Adds file or file input to the queue
			 * @returns id
			 **/
			add: function(file) {
			},
			/**
			 * Sends the file identified by id and additional query params to the server
			 */
			upload: function(id, data) {
				if (this.exist(id)) {
					var len = this._queue.push(id);

					var copy = {};
					$.extend(copy, $.isFunction(data) ? data.call() : data);
					this._data[id] = copy;

					// if too many active uploads, wait...
					if (len <= this._options.maxConnections) {
						var name = this.getName(id);
						$(this._options.element).trigger('uploadStarted', [id, name]);
						this._upload(id, this._data[id]);
					}
				}
			},
			/**
			 * Cancels file upload by id
			 */
			cancel: function(id) {
				$(this._options.element).trigger('uploadCanceled', [id]);
				this._cancel(id);
				this._dequeue(id);
			},
			/**
			 * Cancells all uploads
			 */
			cancelAll: function() {
				for (var i = 0; i < this._queue.length; i++) {
					this._cancel(this._queue[i]);
				}
				this._queue = [];
			},
			/**
			 * Returns name of the file identified by id
			 */
			getName: function(id) {
			},
			/**
			 * Returns size of the file identified by id
			 */
			getSize: function(id) {
			},
			/**
			 * Returns id of files being uploaded or
			 * waiting for their turn
			 */
			getQueue: function() {
				return this._queue;
			},
			/**
			 * Actual upload method
			 */
			_upload: function(id) {
			},
			/**
			 * Actual cancel method
			 */
			_cancel: function(id) {
			},
			/**
			 * Removes element from queue, starts upload of next
			 */
			_dequeue: function(id) {
				var needle = parseInt(id);
				if (!needle && (needle != 0)) needle = id;
				var i = $.inArray(needle, this._queue);
				if (i > -1) this._queue.splice(i, 1);

				var max = this._options.maxConnections;

				if (this._queue.length >= max) {
					var nextId = this._queue[max - 1];
					this._upload(nextId, this._data[nextId]);
				}

				if ((this._queue.length == 0) && (i > -1)) {
					$(this._options.element).trigger('uploadListFinished', [this.last_response]);
					this._options.onListComplete(this.last_response);
				}
			}
		};

		var getUniqueId = (function() {
			var id = 0;
			return function() {
				return id++;
			};
		})();

		/**
		 * Class for uploading files using form and iframe
		 * @inherits UploadHandlerAbstract
		 */
		UploadHandlerForm = function(o) {
			UploadHandlerAbstract.apply(this, arguments);

			this._inputs = {};
		};
		// @inherits UploadHandlerAbstract
		$.extend(UploadHandlerForm.prototype, UploadHandlerAbstract.prototype);

		$.extend(UploadHandlerForm.prototype, {
			add: function(fileInput) {
				fileInput.attr('name', this._options.name);
				var id = 'qq-upload-handler-iframe' + getUniqueId();

				this._inputs[id] = fileInput;

				// remove file input from DOM
				if (fileInput.parentNode) {
					$(fileInput).remove();
				}

				return id;
			},
			fileCount: function() {
				return this._inputs ? this._inputs.length : 0;
			},
			exist: function(id) {
				return (this._inputs && this._inputs[id])
			},
			getName: function(id) {
				// get input value and remove path to normalize
				return this._inputs[id].val().replace(/.*(\/|\\)/, "");
			},
			_cancel: function(id) {
				var name = this.getName(id);
				this._options.onCancel(id, name, this.fileCount());
				$(this._options.element).trigger('uploadCanceled', [id, name]);

				delete this._inputs[id];

				var iframe = $("#" + id);
				if (iframe) {
					// to cancel request set src to something else
					// we use src="javascript:false;" because it doesn't
					// trigger ie6 prompt on https
					iframe.attr('src', 'javascript:false;');

					iframe.remove();
				}
			},
			_upload: function(id, data) {
				var input = this._inputs[id];

				if (!input) {
					throw new Error('file with passed id was not added, or already uploaded or cancelled');
				}

				var fileName = this.getName(id);

				var iframe = this._createIframe(id);
				var form = this._createForm(iframe, data);
				form.append(input);

				var self = this;
				this._attachLoadEvent(iframe, function() {
					self.log('iframe loaded');

					self.last_response = self._getIframeContentJSON(iframe);

					$(self._options.element).trigger('uploadFinished', [id, fileName, self.last_response]);
					self._options.onComplete(id, fileName, self.last_response);
					self._dequeue(id);

					delete self._inputs[id];
					// timeout added to fix busy state in FF3.6
					setTimeout(function() {
						iframe.remove();
					}, 1);
				});

				form.submit();
				form.remove();

				return id;
			},
			_attachLoadEvent: function(iframe, callback) {
				iframe.load(function() {
					var iframe_element = iframe.get(0);
					// when we remove iframe from dom
					// the request stops, but in IE load
					// event fires
					if (!iframe.get(0).parentNode) {
						return;
					}

					// fixing Opera 10.53
					if (iframe_element.contentDocument &&
							iframe_element.contentDocument.body &&
							iframe_element.contentDocument.body.innerHTML == "false") {
						// In Opera event is fired second time
						// when body.innerHTML changed from false
						// to server response approx. after 1 sec
						// when we upload file with iframe
						return;
					}

					callback();
				});
			},
			/**
			 * Returns json object received by iframe from server.
			 */
			_getIframeContentJSON: function(iframe) {
				// iframe.contentWindow.document - for IE<7
				var iframe_element = iframe.get(0);
				var doc = iframe_element.contentDocument ? iframe_element.contentDocument : iframe_element.contentWindow.document,
						response;

				this.log("converting iframe's innerHTML to JSON");
				this.log("innerHTML = " + doc.body.innerHTML);

				try {
					response = $.parseJSON(doc.body.innerHTML);
				} catch(err) {
					response = doc.body.innerHTML;
				}

				return response;
			},
			/**
			 * Creates iframe with unique name
			 */
			_createIframe: function(id) {
				// We can't use following code as the name attribute
				// won't be properly registered in IE6, and new window
				// on form submit will open
				// var iframe = document.createElement('iframe');
				// iframe.setAttribute('name', id);

				var iframe = $('<iframe src="javascript:false;" name="' + id + '" />');
				// src="javascript:false;" removes ie6 prompt on https

				iframe.attr('id', id);
				iframe.hide();
				$('body').append(iframe);
				return iframe;
			},
			/**
			 * Creates form, that will be submitted to iframe
			 */
			_createForm: function(iframe, data) {
				// We can't use the following code in IE6
				// var form = document.createElement('form');
				// form.setAttribute('method', 'post');
				// form.setAttribute('enctype', 'multipart/form-data');
				// Because in this case file won't be attached to request
				var form = $('<form method="post" enctype="multipart/form-data"></form>');

				var queryString = this._options.action + "?" + $.param(data);

				form.attr('action', queryString);
				form.attr('target', iframe.attr('name'));
				form.hide();
				$(document.body).append(form);

				return form;
			}
		});

		/**
		 * Class for uploading files using xhr
		 * @inherits UploadHandlerAbstract
		 */
		UploadHandlerXhr = function(o) {
			UploadHandlerAbstract.apply(this, arguments);

			this._files = [];
			this._xhrs = [];

			// current loaded size in bytes for each file
			this._loaded = [];
		};

		// static method
		UploadHandlerXhr.isSupported = function() {
			var input = document.createElement('input');
			input.type = 'file';

			return (
					'multiple' in input &&
					typeof File != "undefined" &&
					typeof (new XMLHttpRequest()).upload != "undefined" );
		};

		// @inherits UploadHandlerAbstract
		$.extend(UploadHandlerXhr.prototype, UploadHandlerAbstract.prototype)

		$.extend(UploadHandlerXhr.prototype, {
			isXhrHandler: true,
			/**
			 * Adds file to the queue
			 * Returns id to use with upload, cancel
			 **/
			add: function(file) {
				if (!(file instanceof File)) {
					throw new Error('Passed obj in not a File (in UploadHandlerXhr)');
				}

				return this._files.push(file) - 1;
			},
			fileCount: function() {
				return (this._files ? this._files.length : 0);
			},
			exist: function(id) {
				return (this._files && this._files[id]);
			},
			getName: function(id) {
				var file = this._files[id];
				// fix missing name in Safari 4
				return file.fileName != null ? file.fileName : file.name;
			},
			getSize: function(id) {
				var file = this._files[id];
				return file.fileSize != null ? file.fileSize : file.size;
			},
			/**
			 * Returns uploaded bytes for file identified by id
			 */
			getLoaded: function(id) {
				return this._loaded[id] || 0;
			},
			/**
			 * Sends the file identified by id and additional query params to the server
			 * @param {Object} params name-value string pairs
			 */
			_upload: function(id, data) {
				var file = this._files[id];
				// upload can be canceled for this file
				if (!file) return;
				var name = this.getName(id),
						size = this.getSize(id);

				this._loaded[id] = 0;

				var xhr = this._xhrs[id] = new XMLHttpRequest();
				var self = this;

				xhr.upload.onprogress = function(e) {
					if (e.lengthComputable) {
						self._loaded[id] = e.loaded;
						self._options.onProgress(id, name, e.loaded, e.total);
					}
				};

				xhr.onreadystatechange = function() {
					if (xhr.readyState == 4) {
						self._onComplete(id, xhr);
					}
				};

				// build query string
				data = data || {};
				data['raw_upload_param_name'] = self._options.name
				var queryString = this._options.action + "?" + $.param(data);

				xhr.open("POST", queryString, true);
				xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
				xhr.setRequestHeader("X-File-Name", encodeURIComponent(name));
				xhr.setRequestHeader("Content-Type", "application/octet-stream");
				xhr.send(file);
			},
			_onComplete: function(id, xhr) {
				// the request was aborted/cancelled
				if (!this._files[id]) return;

				var name = this.getName(id);
				var size = this.getSize(id);

				this._options.onProgress(id, name, size, size);

				this.last_response = null;
				if (xhr.status == 200) {
					try {
						var json =  $.parseJSON(xhr.responseText);
						this.last_response = json;
					} catch(err) {
						this.last_response = xhr.responseText;
					}
				} else {
					this.last_response = xhr.responseText;
				}

				this.log("xhr - server response received");
				this.log("responseText = " + xhr.responseText);
				$(this._options.element).trigger('uploadFinished', [id, name, this.last_response]);
				this._options.onComplete(id, name, this.last_response);

				this._files[id] = null;
				this._xhrs[id] = null;
				this._dequeue(id);
			},
			_cancel: function(id) {
				this._options.onCancel(id, this.getName(id), this.fileCount());
				$(this._options.element).trigger('uploadFinished', [id, name, {}]);

				this._files[id] = null;

				if (this._xhrs[id]) {
					this._xhrs[id].abort();
					this._xhrs[id] = null;
				}
			}
		});

		return this.each(function() {
			options.element = this;
			var uploader = new FileUploader(options);
		});

	} // end of fileuploader function

})(jQuery);


