/**
 * plupload.js
 *
 * Copyright 2009, Moxiecode Systems AB
 * Released under GPL License.
 *
 * License: http://www.plupload.com/license
 * Contributing: http://www.plupload.com/contributing
 */

// JSLint defined globals
/*global window:false, escape:false */

/*!@@version@@*/

(function() {
	var count = 0, runtimes = [], i18n = {}, mimes = {},
			xmlEncodeChars = {'<' : 'lt', '>' : 'gt', '&' : 'amp', '"' : 'quot', '\'' : '#39'},
			xmlEncodeRegExp = /[<>&\"\']/g, undef, delay = window.setTimeout,
		// A place to store references to event handlers
			eventhash = {},
			uid;

	// IE W3C like event funcs
	function preventDefault() {
		this.returnValue = false;
	}

	function stopPropagation() {
		this.cancelBubble = true;
	}

	// Parses the default mime types string into a mimes lookup map
	(function(mime_data) {
		var items = mime_data.split(/,/), i, y, ext;

		for (i = 0; i < items.length; i += 2) {
			ext = items[i + 1].split(/ /);

			for (y = 0; y < ext.length; y++) {
				mimes[ext[y]] = items[i];
			}
		}
	})(
			"application/msword,doc dot," +
					"application/pdf,pdf," +
					"application/pgp-signature,pgp," +
					"application/postscript,ps ai eps," +
					"application/rtf,rtf," +
					"application/vnd.ms-excel,xls xlb," +
					"application/vnd.ms-powerpoint,ppt pps pot," +
					"application/zip,zip," +
					"application/x-shockwave-flash,swf swfl," +
					"application/vnd.openxmlformats,docx pptx xlsx," +
					"audio/mpeg,mpga mpega mp2 mp3," +
					"audio/x-wav,wav," +
					"audio/mp4,m4a," +
					"image/bmp,bmp," +
					"image/gif,gif," +
					"image/jpeg,jpeg jpg jpe," +
					"image/png,png," +
					"image/svg+xml,svg svgz," +
					"image/tiff,tiff tif," +
					"text/html,htm html xhtml," +
					"text/rtf,rtf," +
					"video/mpeg,mpeg mpg mpe," +
					"video/quicktime,qt mov," +
					"video/mp4,mp4," +
					"video/x-m4v,m4v," +
					"video/x-flv,flv," +
					"video/vnd.rn-realvideo,rv," +
					"text/plain,asc txt text diff log," +
					"application/octet-stream,exe"
	);

	/**
	 * Plupload class with some global constants and functions.
	 *
	 * @example
	 * // Encode entities
	 * console.log(plupload.xmlEncode("My string &lt;&gt;"));
	 *
	 * // Generate unique id
	 * console.log(plupload.guid());
	 *
	 * @static
	 * @class plupload
	 */
	var plupload = {
		/**
		 * Plupload version will be replaced on build.
		 */
		VERSION : '@@version@@',

		/**
		 * Inital state of the queue and also the state ones it's finished all it's uploads.
		 *
		 * @property STOPPED
		 * @final
		 */
		STOPPED : 1,

		/**
		 * Upload process is running
		 *
		 * @property STARTED
		 * @final
		 */
		STARTED : 2,

		/**
		 * File is queued for upload
		 *
		 * @property QUEUED
		 * @final
		 */
		QUEUED : 1,

		/**
		 * File is waiting for upload
		 *
		 * @property WAITING
		 * @final
		 */
		WAITING : 6,

		/**
		 * File is being uploaded
		 *
		 * @property UPLOADING
		 * @final
		 */
		UPLOADING : 2,

		/**
		 * File has failed to be uploaded
		 *
		 * @property FAILED
		 * @final
		 */
		FAILED : 4,

		/**
		 * File has been uploaded successfully
		 *
		 * @property DONE
		 * @final
		 */
		DONE : 5,

		// Error constants used by the Error event

		/**
		 * Generic error for example if an exception is thrown inside Silverlight.
		 *
		 * @property GENERIC_ERROR
		 * @final
		 */
		GENERIC_ERROR : -100,

		/**
		 * HTTP transport error. For example if the server produces a HTTP status other than 200.
		 *
		 * @property HTTP_ERROR
		 * @final
		 */
		HTTP_ERROR : -200,

		/**
		 * Generic I/O error. For exampe if it wasn't possible to open the file stream on local machine.
		 *
		 * @property IO_ERROR
		 * @final
		 */
		IO_ERROR : -300,

		/**
		 * Generic I/O error. For exampe if it wasn't possible to open the file stream on local machine.
		 *
		 * @property SECURITY_ERROR
		 * @final
		 */
		SECURITY_ERROR : -400,

		/**
		 * Initialization error. Will be triggered if no runtime was initialized.
		 *
		 * @property INIT_ERROR
		 * @final
		 */
		INIT_ERROR : -500,

		/**
		 * File size error. If the user selects a file that is to large it will be blocked and an error of this type will be triggered.
		 *
		 * @property FILE_SIZE_ERROR
		 * @final
		 */
		FILE_SIZE_ERROR : -600,

		/**
		 * File extension error. If the user selects a file that isn't valid according to the filters setting.
		 *
		 * @property FILE_EXTENSION_ERROR
		 * @final
		 */
		FILE_EXTENSION_ERROR : -601,

		/**
		 * Runtime will try to detect if image is proper one. Otherwise will throw this error.
		 *
		 * @property IMAGE_FORMAT_ERROR
		 * @final
		 */
		IMAGE_FORMAT_ERROR : -700,

		/**
		 * While working on the image runtime will try to detect if the operation may potentially run out of memeory and will throw this error.
		 *
		 * @property IMAGE_MEMORY_ERROR
		 * @final
		 */
		IMAGE_MEMORY_ERROR : -701,

		/**
		 * Each runtime has an upper limit on a dimension of the image it can handle. If bigger, will throw this error.
		 *
		 * @property IMAGE_DIMENSIONS_ERROR
		 * @final
		 */
		IMAGE_DIMENSIONS_ERROR : -702,


		/**
		 * Mime type lookup table.
		 *
		 * @property mimeTypes
		 * @type Object
		 * @final
		 */
		mimeTypes : mimes,

		/**
		 * Extends the specified object with another object.
		 *
		 * @method extend
		 * @param {Object} target Object to extend.
		 * @param {Object..} obj Multiple objects to extend with.
		 * @return {Object} Same as target, the extended object.
		 */
		extend : function(target) {
			plupload.each(arguments, function(arg, i) {
				if (i > 0) {
					plupload.each(arg, function(value, key) {
						target[key] = value;
					});
				}
			});

			return target;
		},

		/**
		 * Cleans the specified name from national characters (diacritics). The result will be a name with only a-z, 0-9 and _.
		 *
		 * @method cleanName
		 * @param {String} s String to clean up.
		 * @return {String} Cleaned string.
		 */
		cleanName : function(name) {
			var i, lookup;

			// Replace diacritics
			lookup = [
				/[\300-\306]/g, 'A', /[\340-\346]/g, 'a',
				/\307/g, 'C', /\347/g, 'c',
				/[\310-\313]/g, 'E', /[\350-\353]/g, 'e',
				/[\314-\317]/g, 'I', /[\354-\357]/g, 'i',
				/\321/g, 'N', /\361/g, 'n',
				/[\322-\330]/g, 'O', /[\362-\370]/g, 'o',
				/[\331-\334]/g, 'U', /[\371-\374]/g, 'u'
			];

			for (i = 0; i < lookup.length; i += 2) {
				name = name.replace(lookup[i], lookup[i + 1]);
			}

			// Replace whitespace
			name = name.replace(/\s+/g, '_');

			// Remove anything else
			name = name.replace(/[^a-z0-9_\-\.]+/gi, '');

			return name;
		},

		/**
		 * Adds a specific upload runtime like for example flash or gears.
		 *
		 * @method addRuntime
		 * @param {String} name Runtime name for example flash.
		 * @param {Object} obj Object containing init/destroy method.
		 */
		addRuntime : function(name, runtime) {
			runtime.name = name;
			runtimes[name] = runtime;
			runtimes.push(runtime);

			return runtime;
		},

		/**
		 * Generates an unique ID. This is 99.99% unique since it takes the current time and 5 random numbers.
		 * The only way a user would be able to get the same ID is if the two persons at the same exact milisecond manages
		 * to get 5 the same random numbers between 0-65535 it also uses a counter so each call will be guaranteed to be page unique.
		 * It's more probable for the earth to be hit with an ansteriod. You can also if you want to be 100% sure set the plupload.guidPrefix property
		 * to an user unique key.
		 *
		 * @method guid
		 * @return {String} Virtually unique id.
		 */
		guid : function() {
			var guid = new Date().getTime().toString(32), i;

			for (i = 0; i < 5; i++) {
				guid += Math.floor(Math.random() * 65535).toString(32);
			}

			return (plupload.guidPrefix || 'p') + guid + (count++).toString(32);
		},

		/**
		 * Builds a full url out of a base URL and an object with items to append as query string items.
		 *
		 * @param {String} url Base URL to append query string items to.
		 * @param {Object} items Name/value object to serialize as a querystring.
		 * @return {String} String with url + serialized query string items.
		 */
		buildUrl : function(url, items) {
			var query = '';

			plupload.each(items, function(value, name) {
				query += (query ? '&' : '') + encodeURIComponent(name) + '=' + encodeURIComponent(value);
			});

			if (query) {
				url += (url.indexOf('?') > 0 ? '&' : '?') + query;
			}

			return url;
		},

		/**
		 * Executes the callback function for each item in array/object. If you return false in the
		 * callback it will break the loop.
		 *
		 * @param {Object} obj Object to iterate.
		 * @param {function} callback Callback function to execute for each item.
		 */
		each : function(obj, callback) {
			var length, key, i;

			if (obj) {
				length = obj.length;

				if (length === undef) {
					// Loop object items
					for (key in obj) {
						if (obj.hasOwnProperty(key)) {
							if (callback(obj[key], key) === false) {
								return;
							}
						}
					}
				} else {
					// Loop array items
					for (i = 0; i < length; i++) {
						if (callback(obj[i], i) === false) {
							return;
						}
					}
				}
			}
		},

		/**
		 * Formats the specified number as a size string for example 1024 becomes 1 KB.
		 *
		 * @method formatSize
		 * @param {Number} size Size to format as string.
		 * @return {String} Formatted size string.
		 */
		formatSize : function(size) {
			if (size === undef || /\D/.test(size)) {
				return plupload.translate('N/A');
			}

			// GB
			if (size > 1073741824) {
				return Math.round(size / 1073741824, 1) + " GB";
			}

			// MB
			if (size > 1048576) {
				return Math.round(size / 1048576, 1) + " MB";
			}

			// KB
			if (size > 1024) {
				return Math.round(size / 1024, 1) + " KB";
			}

			return size + " b";
		},

		/**
		 * Returns the absolute x, y position of an Element. The position will be returned in a object with x, y fields.
		 *
		 * @method getPos
		 * @param {Element} node HTML element or element id to get x, y position from.
		 * @param {Element} root Optional root element to stop calculations at.
		 * @return {object} Absolute position of the specified element object with x, y fields.
		 */
		getPos : function(node, root) {
			var x = 0, y = 0, parent, doc = document, nodeRect, rootRect;

			node = node;
			root = root || doc.body;

			// Returns the x, y cordinate for an element on IE 6 and IE 7
			function getIEPos(node) {
				var bodyElm, rect, x = 0, y = 0;

				if (node) {
					rect = node.getBoundingClientRect();
					bodyElm = doc.compatMode === "CSS1Compat" ? doc.documentElement : doc.body;
					x = rect.left + bodyElm.scrollLeft;
					y = rect.top + bodyElm.scrollTop;
				}

				return {
					x : x,
					y : y
				};
			}

			// Use getBoundingClientRect on IE 6 and IE 7 but not on IE 8 in standards mode
			if (node && node.getBoundingClientRect && (navigator.userAgent.indexOf('MSIE') > 0 && doc.documentMode !== 8)) {
				nodeRect = getIEPos(node);
				rootRect = getIEPos(root);

				return {
					x : nodeRect.x - rootRect.x,
					y : nodeRect.y - rootRect.y
				};
			}

			parent = node;
			while (parent && parent != root && parent.nodeType) {
				x += parent.offsetLeft || 0;
				y += parent.offsetTop || 0;
				parent = parent.offsetParent;
			}

			parent = node.parentNode;
			while (parent && parent != root && parent.nodeType) {
				x -= parent.scrollLeft || 0;
				y -= parent.scrollTop || 0;
				parent = parent.parentNode;
			}

			return {
				x : x,
				y : y
			};
		},

		/**
		 * Returns the size of the specified node in pixels.
		 *
		 * @param {Node} node Node to get the size of.
		 * @return {Object} Object with a w and h property.
		 */
		getSize : function(node) {
			return {
				w : node.offsetWidth || node.clientWidth,
				h : node.offsetHeight || node.clientHeight
			};
		},

		/**
		 * Parses the specified size string into a byte value. For example 10kb becomes 10240.
		 *
		 * @method parseSize
		 * @param {String/Number} size String to parse or number to just pass through.
		 * @return {Number} Size in bytes.
		 */
		parseSize : function(size) {
			var mul;

			if (typeof(size) == 'string') {
				size = /^([0-9]+)([mgk]+)$/.exec(size.toLowerCase().replace(/[^0-9mkg]/g, ''));
				mul = size[2];
				size = +size[1];

				if (mul == 'g') {
					size *= 1073741824;
				}

				if (mul == 'm') {
					size *= 1048576;
				}

				if (mul == 'k') {
					size *= 1024;
				}
			}

			return size;
		},

		/**
		 * Encodes the specified string.
		 *
		 * @method xmlEncode
		 * @param {String} s String to encode.
		 * @return {String} Encoded string.
		 */
		xmlEncode : function(str) {
			return str ? ('' + str).replace(xmlEncodeRegExp, function(chr) {
				return xmlEncodeChars[chr] ? '&' + xmlEncodeChars[chr] + ';' : chr;
			}) : str;
		},

		/**
		 * Forces anything into an array.
		 *
		 * @method toArray
		 * @param {Object} obj Object with length field.
		 * @return {Array} Array object containing all items.
		 */
		toArray : function(obj) {
			var i, arr = [];

			for (i = 0; i < obj.length; i++) {
				arr[i] = obj[i];
			}

			return arr;
		},

		/**
		 * Extends the language pack object with new items.
		 *
		 * @param {Object} pack Language pack items to add.
		 * @return {Object} Extended language pack object.
		 */
		addI18n : function(pack) {
			return plupload.extend(i18n, pack);
		},

		/**
		 * Translates the specified string by checking for the english string in the language pack lookup.
		 *
		 * @param {String} str String to look for.
		 * @return {String} Translated string or the input string if it wasn't found.
		 */
		translate : function(str) {
			return i18n[str] || str;
		},

		/**
		 * Checks if object is empty.
		 *
		 * @param {Object} obj Object to check.
		 * @return {Boolean}
		 */
		isEmptyObj : function(obj) {
			if (obj === undef) return true;

			for (var prop in obj) {
				return false;
			}
			return true;
		},

		/**
		 * Checks if specified DOM element has specified class.
		 *
		 * @param {Object} obj DOM element like object to add handler to.
		 * @param {String} name Class name
		 */
		hasClass : function(obj, name) {
			var regExp;

			if (obj.className == '') {
				return false;
			}

			regExp = new RegExp("(^|\\s+)" + name + "(\\s+|$)");

			return regExp.test(obj.className);
		},

		/**
		 * Adds specified className to specified DOM element.
		 *
		 * @param {Object} obj DOM element like object to add handler to.
		 * @param {String} name Class name
		 */
		addClass : function(obj, name) {
			if (!plupload.hasClass(obj, name)) {
				obj.className = obj.className == '' ? name : obj.className.replace(/\s+$/, '') + ' ' + name;
			}
		},

		/**
		 * Removes specified className from specified DOM element.
		 *
		 * @param {Object} obj DOM element like object to add handler to.
		 * @param {String} name Class name
		 */
		removeClass : function(obj, name) {
			var regExp = new RegExp("(^|\\s+)" + name + "(\\s+|$)");

			obj.className = obj.className.replace(regExp, function($0, $1, $2) {
				return $1 === ' ' && $2 === ' ' ? ' ' : '';
			});
		},

		/**
		 * Returns a given computed style of a DOM element.
		 *
		 * @param {Object} obj DOM element like object.
		 * @param {String} name Style you want to get from the DOM element
		 */
		getStyle : function(obj, name) {
			if (obj.currentStyle) {
				return obj.currentStyle[name];
			} else if (window.getComputedStyle) {
				return window.getComputedStyle(obj, null)[name];
			}
		},

		/**
		 * Adds an event handler to the specified object and store reference to the handler
		 * in objects internal Plupload registry (@see removeEvent).
		 *
		 * @param {Object} obj DOM element like object to add handler to.
		 * @param {String} name Name to add event listener to.
		 * @param {Function} callback Function to call when event occurs.
		 * @param {String} (optional) key that might be used to add specifity to the event record.
		 */
		addEvent : function(obj, name, callback) {
			var func, events, types, key;

			// if passed in, event will be locked with this key - one would need to provide it to removeEvent
			key = arguments[3];

			name = name.toLowerCase();

			// Initialize unique identifier if needed
			if (uid === undef) {
				uid = 'Plupload_' + plupload.guid();
			}

			// Add event listener
			if (obj.attachEvent) {

				func = function() {
					var evt = window.event;

					if (!evt.target) {
						evt.target = evt.srcElement;
					}

					evt.preventDefault = preventDefault;
					evt.stopPropagation = stopPropagation;

					callback(evt);
				};
				obj.attachEvent('on' + name, func);

			} else if (obj.addEventListener) {
				func = callback;

				obj.addEventListener(name, func, false);
			}

			// Log event handler to objects internal Plupload registry
			if (obj[uid] === undef) {
				obj[uid] = plupload.guid();
			}

			if (!eventhash.hasOwnProperty(obj[uid])) {
				eventhash[obj[uid]] = {};
			}

			events = eventhash[obj[uid]];

			if (!events.hasOwnProperty(name)) {
				events[name] = [];
			}

			events[name].push({
				func: func,
				orig: callback, // store original callback for IE
				key: key
			});
		},


		/**
		 * Remove event handler from the specified object. If third argument (callback)
		 * is not specified remove all events with the specified name.
		 *
		 * @param {Object} obj DOM element to remove event listener(s) from.
		 * @param {String} name Name of event listener to remove.
		 * @param {Function|String} (optional) might be a callback or unique key to match.
		 */
		removeEvent: function(obj, name) {
			var type, callback, key;

			// match the handler either by callback or by key
			if (typeof(arguments[2]) == "function") {
				callback = arguments[2];
			} else {
				key = arguments[2];
			}

			name = name.toLowerCase();

			if (obj[uid] && eventhash[obj[uid]] && eventhash[obj[uid]][name]) {
				type = eventhash[obj[uid]][name];
			} else {
				return;
			}


			for (var i = type.length - 1; i >= 0; i--) {
				// undefined or not, key should match
				if (type[i].key === key || type[i].orig === callback) {

					if (obj.detachEvent) {
						obj.detachEvent('on' + name, type[i].func);
					} else if (obj.removeEventListener) {
						obj.removeEventListener(name, type[i].func, false);
					}

					type[i].orig = null;
					type[i].func = null;

					type.splice(i, 1);

					// If callback was passed we are done here, otherwise proceed
					if (callback !== undef) {
						break;
					}
				}
			}

			// If event array got empty, remove it
			if (!type.length) {
				delete eventhash[obj[uid]][name];
			}

			// If Plupload registry has become empty, remove it
			if (plupload.isEmptyObj(eventhash[obj[uid]])) {
				delete eventhash[obj[uid]];

				// IE doesn't let you remove DOM object property with - delete
				try {
					delete obj[uid];
				} catch(e) {
					obj[uid] = undef;
				}
			}
		},


		/**
		 * Remove all kind of events from the specified object
		 *
		 * @param {Object} obj DOM element to remove event listeners from.
		 * @param {String} (optional) unique key to match, when removing events.
		 */
		removeAllEvents: function(obj) {
			var key = arguments[1];

			if (obj[uid] === undef || !obj[uid]) {
				return;
			}

			plupload.each(eventhash[obj[uid]], function(events, name) {
				plupload.removeEvent(obj, name, key);
			});
		}
	};


	/**
	 * Uploader class, an instance of this class will be created for each upload field.
	 *
	 * @example
	 * var uploader = new plupload.Uploader({
	 *	 runtimes : 'gears,html5,flash',
	 *	 browse_button : 'button_id'
	 * });
	 *
	 * uploader.bind('Init', function(up) {
	 *	 alert('Supports drag/drop: ' + (!!up.features.dragdrop));
	 * });
	 *
	 * uploader.bind('FilesAdded', function(up, files) {
	 *	 alert('Selected files: ' + files.length);
	 * });
	 *
	 * uploader.bind('QueueChanged', function(up) {
	 *	 alert('Queued files: ' + uploader.files.length);
	 * });
	 *
	 * uploader.init();
	 *
	 * @class plupload.Uploader
	 */

	/**
	 * Constructs a new uploader instance.
	 *
	 * @constructor
	 * @method Uploader
	 * @param {Object} settings Initialization settings, to be used by the uploader instance and runtimes.
	 */
	plupload.Uploader = function(settings) {
		var events = {}, total, files = [], startTime;

		// Inital total state
		total = new plupload.QueueProgress();

		// Default settings
		settings = plupload.extend({
			chunk_size : 0,
			multipart : true,
			multi_selection : true,
			file_data_name : 'file',
			filters : []
		}, settings);

		// Private methods
		function uploadNext() {
			var file, count = 0, i;

			if (this.state == plupload.STARTED) {
				// Find first QUEUED file
				for (i = 0; i < files.length; i++) {
					if (!file && files[i].status == plupload.QUEUED) {
						file = files[i];
						file.status = plupload.UPLOADING;
						this.trigger("BeforeUpload", file);
						this.trigger("UploadFile", file);
					} else {
						count++;
					}
				}

				// All files are DONE or FAILED
				if (count == files.length) {
					this.trigger("UploadComplete", files);
					this.stop();
				}
			}
		}

		function calc() {
			var i, file;

			// Reset stats
			total.reset();

			// Check status, size, loaded etc on all files
			for (i = 0; i < files.length; i++) {
				file = files[i];

				if (file.size !== undef) {
					total.size += file.size;
					total.loaded += file.loaded;
				} else {
					total.size = undef;
				}

				if (file.status == plupload.DONE) {
					total.uploaded++;
				} else if (file.status == plupload.FAILED) {
					total.failed++;
				} else {
					total.queued++;
				}
			}

			// If we couldn't calculate a total file size then use the number of files to calc percent
			if (total.size === undef) {
				total.percent = files.length > 0 ? Math.ceil(total.uploaded / files.length * 100) : 0;
			} else {
				total.bytesPerSec = Math.ceil(total.loaded / ((+new Date() - startTime || 1) / 1000.0));
				total.percent = total.size > 0 ? Math.ceil(total.loaded / total.size * 100) : 0;
			}
		}

		// Add public methods
		plupload.extend(this, {
			/**
			 * Current state of the total uploading progress. This one can either be plupload.STARTED or plupload.STOPPED.
			 * These states are controlled by the stop/start methods. The default value is STOPPED.
			 *
			 * @property state
			 * @type Number
			 */
			state : plupload.STOPPED,

			/**
			 * Current runtime name.
			 *
			 * @property runtime
			 * @type String
			 */
			runtime: '',

			/**
			 * Map of features that are available for the uploader runtime. Features will be filled
			 * before the init event is called, these features can then be used to alter the UI for the end user.
			 * Some of the current features that might be in this map is: dragdrop, chunks, jpgresize, pngresize.
			 *
			 * @property features
			 * @type Object
			 */
			features : {},

			/**
			 * Current upload queue, an array of File instances.
			 *
			 * @property files
			 * @type Array
			 * @see plupload.File
			 */
			files : files,

			/**
			 * Object with name/value settings.
			 *
			 * @property settings
			 * @type Object
			 */
			settings : settings,

			/**
			 * Total progess information. How many files has been uploaded, total percent etc.
			 *
			 * @property total
			 * @type plupload.QueueProgress
			 */
			total : total,

			/**
			 * Unique id for the Uploader instance.
			 *
			 * @property id
			 * @type String
			 */
			id : plupload.guid(),

			/**
			 * Initializes the Uploader instance and adds internal event listeners.
			 *
			 * @method init
			 */
			init : function() {
				var self = this, i, runtimeList, a, runTimeIndex = 0, items;

				if (typeof(settings.preinit) == "function") {
					settings.preinit(self);
				} else {
					plupload.each(settings.preinit, function(func, name) {
						self.bind(name, func);
					});
				}

				settings.page_url = settings.page_url || document.location.pathname.replace(/\/[^\/]+$/g, '/');

				// If url is relative force it absolute to the current page
				if (!/^(\w+:\/\/|\/)/.test(settings.url)) {
					settings.url = settings.page_url + settings.url;
				}

				// Convert settings
				settings.chunk_size = plupload.parseSize(settings.chunk_size);
				settings.max_file_size = plupload.parseSize(settings.max_file_size);

				// Add files to queue
				self.bind('FilesAdded', function(up, selected_files) {
					var i, file, count = 0, extensionsRegExp, filters = settings.filters;

					// Convert extensions to regexp
					if (filters && filters.length) {
						extensionsRegExp = [];

						plupload.each(filters, function(filter) {
							plupload.each(filter.extensions.split(/,/), function(ext) {
								if (/^\s*\*\s*$/.test(ext)) {
									extensionsRegExp.push('\\.*');
								} else {
									extensionsRegExp.push('\\.' + ext.replace(new RegExp('[' + ('/^$.*+?|()[]{}\\'.replace(/./g, '\\$&')) + ']', 'g'), '\\$&'));
								}
							});
						});

						extensionsRegExp = new RegExp(extensionsRegExp.join('|') + '$', 'i');
					}

					for (i = 0; i < selected_files.length; i++) {
						file = selected_files[i];
						file.loaded = 0;
						file.percent = 0;
						file.status = plupload.QUEUED;

						// Invalid file extension
						if (extensionsRegExp && !extensionsRegExp.test(file.name)) {
							up.trigger('Error', {
								code : plupload.FILE_EXTENSION_ERROR,
								message : plupload.translate('File extension error.'),
								file : file
							});

							continue;
						}

						// Invalid file size
						if (file.size !== undef && file.size > settings.max_file_size) {
							up.trigger('Error', {
								code : plupload.FILE_SIZE_ERROR,
								message : plupload.translate('File size error.'),
								file : file
							});

							continue;
						}

						// Add valid file to list
						files.push(file);
						count++;
					}

					// Only trigger QueueChanged event if any files where added
					if (count) {
						delay(function() {
							self.trigger("QueueChanged");
							self.refresh();
						}, 1);
					} else {
						return false; // Stop the FilesAdded event from immediate propagation
					}
				});

				// Generate unique target filenames
				if (settings.unique_names) {
					self.bind("UploadFile", function(up, file) {
						var matches = file.name.match(/\.([^.]+)$/), ext = "tmp";

						if (matches) {
							ext = matches[1];
						}

						file.target_name = file.id + '.' + ext;
					});
				}

				self.bind('UploadProgress', function(up, file) {
					file.percent = file.size > 0 ? Math.ceil(file.loaded / file.size * 100) : 100;
					calc();
				});

				self.bind('StateChanged', function(up) {
					if (up.state == plupload.STARTED) {
						// Get start time to calculate bps
						startTime = (+new Date());

					} else if (up.state == plupload.STOPPED) {
						// Reset currently uploading files
						for (i = up.files.length - 1; i >= 0; i--) {
							if (up.files[i].status == plupload.UPLOADING) {
								up.files[i].status = plupload.QUEUED;
								calc();
							}
						}
					}
				});

				self.bind('QueueChanged', calc);

				self.bind("Error", function(up, err) {
					// Set failed status if an error occured on a file
					if (err.file) {
						err.file.status = plupload.FAILED;
						calc();

						// Upload next file but detach it from the error event
						// since other custom listeners might want to stop the queue
						if (up.state == plupload.STARTED) {
							delay(function() {
								uploadNext.call(self);
							}, 1);
						}
					}
				});

				self.bind("FileUploaded", function(up, file) {
					file.status = plupload.DONE;
					file.loaded = file.size;
					up.trigger('UploadProgress', file);

					// Upload next file but detach it from the error event
					// since other custom listeners might want to stop the queue
					delay(function() {
						uploadNext.call(self);
					}, 1);
				});

				// Setup runtimeList
				if (settings.runtimes) {
					runtimeList = [];
					items = settings.runtimes.split(/\s?,\s?/);

					for (i = 0; i < items.length; i++) {
						if (runtimes[items[i]]) {
							runtimeList.push(runtimes[items[i]]);
						}
					}
				} else {
					runtimeList = runtimes;
				}

				// Call init on each runtime in sequence
				function callNextInit() {
					var runtime = runtimeList[runTimeIndex++], features, requiredFeatures, i;

					if (runtime) {
						features = runtime.getFeatures();

						// Check if runtime supports required features
						requiredFeatures = self.settings.required_features;
						if (requiredFeatures) {
							requiredFeatures = requiredFeatures.split(',');

							for (i = 0; i < requiredFeatures.length; i++) {
								// Specified feature doesn't exist
								if (!features[requiredFeatures[i]]) {
									callNextInit();
									return;
								}
							}
						}

						// Try initializing the runtime
						runtime.init(self, function(res) {
							if (res && res.success) {
								// Successful initialization
								self.features = features;
								self.runtime = runtime.name;
								self.trigger('Init', {runtime : runtime.name});
								self.trigger('PostInit');
								self.refresh();
							} else {
								callNextInit();
							}
						});
					} else {
						// Trigger an init error if we run out of runtimes
						self.trigger('Error', {
							code : plupload.INIT_ERROR,
							message : plupload.translate('Init error.')
						});
					}
				}

				callNextInit();

				if (typeof(settings.init) == "function") {
					settings.init(self);
				} else {
					plupload.each(settings.init, function(func, name) {
						self.bind(name, func);
					});
				}
			},

			/**
			 * Refreshes the upload instance by dispatching out a refresh event to all runtimes.
			 * This would for example reposition flash/silverlight shims on the page.
			 *
			 * @method refresh
			 */
			refresh : function() {
				this.trigger("Refresh");
			},

			/**
			 * Starts uploading the queued files.
			 *
			 * @method start
			 */
			start : function() {
				if (this.state != plupload.STARTED) {
					this.state = plupload.STARTED;
					this.trigger("StateChanged");

					uploadNext.call(this);
				}
			},

			/**
			 * Stops the upload of the queued files.
			 *
			 * @method stop
			 */
			stop : function() {
				if (this.state != plupload.STOPPED) {
					this.state = plupload.STOPPED;
					this.trigger("StateChanged");
				}
			},

			/**
			 * Returns the specified file object by id.
			 *
			 * @method getFile
			 * @param {String} id File id to look for.
			 * @return {plupload.File} File object or undefined if it wasn't found;
			 */
			getFile : function(id) {
				var i;

				for (i = files.length - 1; i >= 0; i--) {
					if (files[i].id === id) {
						return files[i];
					}
				}
			},

			/**
			 * Removes a specific file.
			 *
			 * @method removeFile
			 * @param {plupload.File} file File to remove from queue.
			 */
			removeFile : function(file) {
				var i;

				for (i = files.length - 1; i >= 0; i--) {
					if (files[i].id === file.id) {
						return this.splice(i, 1)[0];
					}
				}
			},

			/**
			 * Removes part of the queue and returns the files removed. This will also trigger the FilesRemoved and QueueChanged events.
			 *
			 * @method splice
			 * @param {Number} start (Optional) Start index to remove from.
			 * @param {Number} length (Optional) Lengh of items to remove.
			 * @return {Array} Array of files that was removed.
			 */
			splice : function(start, length) {
				var removed;

				// Splice and trigger events
				removed = files.splice(start === undef ? 0 : start, length === undef ? files.length : length);

				this.trigger("FilesRemoved", removed);
				this.trigger("QueueChanged");

				return removed;
			},

			/**
			 * Dispatches the specified event name and it's arguments to all listeners.
			 *
			 *
			 * @method trigger
			 * @param {String} name Event name to fire.
			 * @param {Object..} Multiple arguments to pass along to the listener functions.
			 */
			trigger : function(name) {
				var list = events[name.toLowerCase()], i, args;

				// console.log(name, arguments);

				if (list) {
					// Replace name with sender in args
					args = Array.prototype.slice.call(arguments);
					args[0] = this;

					// Dispatch event to all listeners
					for (i = 0; i < list.length; i++) {
						// Fire event, break chain if false is returned
						if (list[i].func.apply(list[i].scope, args) === false) {
							return false;
						}
					}
				}

				return true;
			},

			/**
			 * Adds an event listener by name.
			 *
			 * @method bind
			 * @param {String} name Event name to listen for.
			 * @param {function} func Function to call ones the event gets fired.
			 * @param {Object} scope Optional scope to execute the specified function in.
			 */
			bind : function(name, func, scope) {
				var list;

				name = name.toLowerCase();
				list = events[name] || [];
				list.push({func : func, scope : scope || this});
				events[name] = list;
			},

			/**
			 * Removes the specified event listener.
			 *
			 * @method unbind
			 * @param {String} name Name of event to remove.
			 * @param {function} func Function to remove from listener.
			 */
			unbind : function(name) {
				name = name.toLowerCase();

				var list = events[name], i, func = arguments[1];

				if (list) {
					if (func !== undef) {
						for (i = list.length - 1; i >= 0; i--) {
							if (list[i].func === func) {
								list.splice(i, 1);
								break;
							}
						}
					} else {
						list = [];
					}

					// delete event list if it has become empty
					if (!list.length) {
						delete events[name];
					}
				}
			},

			/**
			 * Removes all event listeners.
			 *
			 * @method unbindAll
			 */
			unbindAll : function() {
				var self = this;

				plupload.each(events, function(list, name) {
					self.unbind(name);
				});
			},

			/**
			 * Destroys Plupload instance and cleans after itself.
			 *
			 * @method destroy
			 */
			destroy : function() {
				this.trigger('Destroy');

				// Clean-up after uploader itself
				this.unbindAll();
			}

			/**
			 * Fires when the current RunTime has been initialized.
			 *
			 * @event Init
			 * @param {plupload.Uploader} uploader Uploader instance sending the event.
			 */

			/**
			 * Fires after the init event incase you need to perform actions there.
			 *
			 * @event PostInit
			 * @param {plupload.Uploader} uploader Uploader instance sending the event.
			 */

			/**
			 * Fires when the silverlight/flash or other shim needs to move.
			 *
			 * @event Refresh
			 * @param {plupload.Uploader} uploader Uploader instance sending the event.
			 */

			/**
			 * Fires when the overall state is being changed for the upload queue.
			 *
			 * @event StateChanged
			 * @param {plupload.Uploader} uploader Uploader instance sending the event.
			 */

			/**
			 * Fires when a file is to be uploaded by the runtime.
			 *
			 * @event UploadFile
			 * @param {plupload.Uploader} uploader Uploader instance sending the event.
			 * @param {plupload.File} file File to be uploaded.
			 */

			/**
			 * Fires when just before a file is uploaded. This event enables you to override settings
			 * on the uploader instance before the file is uploaded.
			 *
			 * @event BeforeUpload
			 * @param {plupload.Uploader} uploader Uploader instance sending the event.
			 * @param {plupload.File} file File to be uploaded.
			 */

			/**
			 * Fires when the file queue is changed. In other words when files are added/removed to the files array of the uploader instance.
			 *
			 * @event QueueChanged
			 * @param {plupload.Uploader} uploader Uploader instance sending the event.
			 */

			/**
			 * Fires while a file is being uploaded. Use this event to update the current file upload progress.
			 *
			 * @event UploadProgress
			 * @param {plupload.Uploader} uploader Uploader instance sending the event.
			 * @param {plupload.File} file File that is currently being uploaded.
			 */

			/**
			 * Fires while a file was removed from queue.
			 *
			 * @event FilesRemoved
			 * @param {plupload.Uploader} uploader Uploader instance sending the event.
			 * @param {Array} files Array of files that got removed.
			 */

			/**
			 * Fires while when the user selects files to upload.
			 *
			 * @event FilesAdded
			 * @param {plupload.Uploader} uploader Uploader instance sending the event.
			 * @param {Array} files Array of file objects that was added to queue/selected by the user.
			 */

			/**
			 * Fires when a file is successfully uploaded.
			 *
			 * @event FileUploaded
			 * @param {plupload.Uploader} uploader Uploader instance sending the event.
			 * @param {plupload.File} file File that was uploaded.
			 * @param {Object} response Object with response properties.
			 */

			/**
			 * Fires when file chunk is uploaded.
			 *
			 * @event ChunkUploaded
			 * @param {plupload.Uploader} uploader Uploader instance sending the event.
			 * @param {plupload.File} file File that the chunk was uploaded for.
			 * @param {Object} response Object with response properties.
			 */

			/**
			 * Fires when all files in a queue are uploaded.
			 *
			 * @event UploadComplete
			 * @param {plupload.Uploader} uploader Uploader instance sending the event.
			 * @param {Array} files Array of file objects that was added to queue/selected by the user.
			 */

			/**
			 * Fires when a error occurs.
			 *
			 * @event Error
			 * @param {plupload.Uploader} uploader Uploader instance sending the event.
			 * @param {Object} error Contains code, message and sometimes file and other details.
			 */

			/**
			 * Fires when destroy method is called.
			 *
			 * @event Destroy
			 * @param {plupload.Uploader} uploader Uploader instance sending the event.
			 */
		});
	};

	/**
	 * File instance.
	 *
	 * @class plupload.File
	 * @param {String} name Name of the file.
	 * @param {Number} size File size.
	 */

	/**
	 * Constructs a new file instance.
	 *
	 * @constructor
	 * @method File
	 * @param {String} id Unique file id.
	 * @param {String} name File name.
	 * @param {Number} size File size in bytes.
	 */
	plupload.File = function(id, name, size, thumb_data) {
		var self = this; // Setup alias for self to reduce code size when it's compressed

		/**
		 * File id this is a globally unique id for the specific file.
		 *
		 * @property id
		 * @type String
		 */
		self.id = id;

		/**
		 * File name for example "myfile.gif".
		 *
		 * @property name
		 * @type String
		 */
		self.name = name;

		/**
		 * File size in bytes.
		 *
		 * @property size
		 * @type Number
		 */
		self.size = size;

		/**
		 * Number of bytes uploaded of the files total size.
		 *
		 * @property loaded
		 * @type Number
		 */
		self.loaded = 0;

		/**
		 * Number of percentage uploaded of the file.
		 *
		 * @property percent
		 * @type Number
		 */
		self.percent = 0;

		/**
		 * Status constant matching the plupload states QUEUED, UPLOADING, FAILED, DONE.
		 *
		 * @property status
		 * @type Number
		 * @see plupload
		 */
		self.status = 0;

		// kasparsj
		self.thumb_loaded = 0;
		self.thumb_percent = 0;
		self.thumb_data = thumb_data;
	};

	/**
	 * Runtime class gets implemented by each upload runtime.
	 *
	 * @class plupload.Runtime
	 * @static
	 */
	plupload.Runtime = function() {
		/**
		 * Returns a list of supported features for the runtime.
		 *
		 * @return {Object} Name/value object with supported features.
		 */
		this.getFeatures = function() {
		};

		/**
		 * Initializes the upload runtime. This method should add necessary items to the DOM and register events needed for operation.
		 *
		 * @method init
		 * @param {plupload.Uploader} uploader Uploader instance that needs to be initialized.
		 * @param {function} callback Callback function to execute when the runtime initializes or fails to initialize. If it succeeds an object with a parameter name success will be set to true.
		 */
		this.init = function(uploader, callback) {
		};
	};

	/**
	 * Runtime class gets implemented by each upload runtime.
	 *
	 * @class plupload.QueueProgress
	 */

	/**
	 * Constructs a queue progress.
	 *
	 * @constructor
	 * @method QueueProgress
	 */
	plupload.QueueProgress = function() {
		var self = this; // Setup alias for self to reduce code size when it's compressed

		/**
		 * Total queue file size.
		 *
		 * @property size
		 * @type Number
		 */
		self.size = 0;

		/**
		 * Total bytes uploaded.
		 *
		 * @property loaded
		 * @type Number
		 */
		self.loaded = 0;

		/**
		 * Number of files uploaded.
		 *
		 * @property uploaded
		 * @type Number
		 */
		self.uploaded = 0;

		/**
		 * Number of files failed to upload.
		 *
		 * @property failed
		 * @type Number
		 */
		self.failed = 0;

		/**
		 * Number of files yet to be uploaded.
		 *
		 * @property queued
		 * @type Number
		 */
		self.queued = 0;

		/**
		 * Total percent of the uploaded bytes.
		 *
		 * @property percent
		 * @type Number
		 */
		self.percent = 0;

		/**
		 * Bytes uploaded per second.
		 *
		 * @property bytesPerSec
		 * @type Number
		 */
		self.bytesPerSec = 0;

		/**
		 * Resets the progress to it's initial values.
		 *
		 * @method reset
		 */
		self.reset = function() {
			self.size = self.loaded = self.uploaded = self.failed = self.queued = self.percent = self.bytesPerSec = 0;
		};
	};

	// Create runtimes namespace
	plupload.runtimes = {};

	// Expose plupload namespace
	window.plupload = plupload;
})();

/**
 * plupload.flash.js
 *
 * Copyright 2009, Moxiecode Systems AB
 * Released under GPL License.
 *
 * License: http://www.plupload.com/license
 * Contributing: http://www.plupload.com/contributing
 */

// JSLint defined globals
/*global window:false, document:false, plupload:false, ActiveXObject:false, escape:false */

(function(window, document, plupload, undef) {
	var uploadInstances = {}, initialized = {};

	function getFlashVersion() {
		var version;

		try {
			version = navigator.plugins['Shockwave Flash'];
			version = version.description;
		} catch (e1) {
			try {
				version = new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version');
			} catch (e2) {
				version = '0.0';
			}
		}

		version = version.match(/\d+/g);

		return parseFloat(version[0] + '.' + version[1]);
	}

	plupload.flash = {
		/**
		 * Will be executed by the Flash runtime when it sends out events.
		 *
		 * @param {String} id If for the upload instance.
		 * @param {String} name Event name to trigger.
		 * @param {Object} obj Parameters to be passed with event.
		 */
		trigger : function(id, name, obj) {

			// Detach the call so that error handling in the browser is presented correctly
			setTimeout(function() {
				var uploader = uploadInstances[id], i, args;

				if (uploader) {
					uploader.trigger('Flash:' + name, obj);
				}
			}, 0);
		}
	};

	/**
	 * FlashRuntime implementation. This runtime supports these features: jpgresize, pngresize, chunks.
	 *
	 * @static
	 * @class plupload.runtimes.Flash
	 * @extends plupload.Runtime
	 */
	plupload.runtimes.Flash = plupload.addRuntime("flash", {

		/**
		 * Returns a list of supported features for the runtime.
		 *
		 * @return {Object} Name/value object with supported features.
		 */
		getFeatures : function() {
			return {
				jpgresize: true,
				pngresize: true,
				maxWidth: 8091,
				maxHeight: 8091,
				chunks: true,
				progress: true,
				multipart: true
			};
		},

		/**
		 * Initializes the upload runtime. This method should add necessary items to the DOM and register events needed for operation.
		 *
		 * @method init
		 * @param {plupload.Uploader} uploader Uploader instance that needs to be initialized.
		 * @param {function} callback Callback to execute when the runtime initializes or fails to initialize. If it succeeds an object with a parameter name success will be set to true.
		 */
		init : function(uploader, callback) {
			var browseButton, flashContainer, flashVars, waitCount = 0, container = document.body;

			if (getFlashVersion() < 10) {
				callback({success : false});
				return;
			}

			initialized[uploader.id] = false;
			uploadInstances[uploader.id] = uploader;

			// Find browse button and set to to be relative
			browseButton = document.getElementById(uploader.settings.browse_button);

			// Create flash container and insert it at an absolute position within the browse button
			flashContainer = document.createElement('div');
			flashContainer.id = uploader.id + '_flash_container';

			plupload.extend(flashContainer.style, {
				position : 'absolute',
				top : '0px',
				background : uploader.settings.shim_bgcolor || 'transparent',
				zIndex : 99999,
				width : '100%',
				height : '100%'
			});

			flashContainer.className = 'plupload flash';

			if (uploader.settings.container) {
				container = document.getElementById(uploader.settings.container);
				if (plupload.getStyle(container, 'position') === 'static') {
					container.style.position = 'relative';
				}
			}

			container.appendChild(flashContainer);

			flashVars = 'id=' + escape(uploader.id);

			// Insert the Flash inide the flash container
			flashContainer.innerHTML = '<object id="' + uploader.id + '_flash" width="100%" height="100%" style="outline:0" type="application/x-shockwave-flash" data="' + uploader.settings.flash_swf_url + '">' +
					'<param name="movie" value="' + uploader.settings.flash_swf_url + '" />' +
					'<param name="flashvars" value="' + flashVars + '" />' +
					'<param name="wmode" value="transparent" />' +
					'<param name="allowscriptaccess" value="always" /></object>';

			function getFlashObj() {
				return document.getElementById(uploader.id + '_flash');
			}

			function waitLoad() {

				// Wait for 5 sec
				if (waitCount++ > 5000) {
					callback({success : false});
					return;
				}

				if (!initialized[uploader.id]) {
					setTimeout(waitLoad, 1);
				}
			}

			waitLoad();

			// Fix IE memory leaks
			browseButton = flashContainer = null;

			// Wait for Flash to send init event
			uploader.bind("Flash:Init", function() {
				var lookup = {}, i;

				getFlashObj().setFileFilters(uploader.settings.filters, uploader.settings.multi_selection);

				// Prevent eventual reinitialization of the instance
				if (initialized[uploader.id]) {
					return;
				}
				initialized[uploader.id] = true;

				uploader.bind("Flash:ImageProgress", function(up, flash_file) {
					var file = up.getFile(lookup[flash_file.id]);
					file.size = flash_file.size;
					file.thumb_loaded = flash_file.loaded;
					file.thumb_percent = file.size > 0 ? Math.ceil(file.thumb_loaded / file.size * 100) : 100;

					up.trigger('QueueChanged',file);
				});

				uploader.bind("Flash:ImageComplete", function(up, flash_file) {
					var file = up.getFile(lookup[flash_file.id]);
					file.size = flash_file.size;

					up.trigger('QueueChanged',file);
				});

				uploader.bind("Flash:ThumbProgress", function(up, flash_file) {
					var file = up.getFile(lookup[flash_file.id]);
					file.size = flash_file.size;

					up.trigger('QueueChanged',file);
				});

				uploader.bind("Flash:ThumbComplete", function(up, flash_file) {
					var file = up.getFile(lookup[flash_file.id]);
					file.size = flash_file.size;
					file.thumb_data = flash_file.thumb_data;

					up.trigger('QueueChanged',file);
				});

				uploader.bind("UploadFile", function(up, file) {
					var settings = up.settings, resize = uploader.settings.resize || {};

					getFlashObj().uploadFile(lookup[file.id], settings.url, {
						name : file.target_name || file.name,
						mime : plupload.mimeTypes[file.name.replace(/^.+\.([^.]+)/, '$1').toLowerCase()] || 'application/octet-stream',
						chunk_size : settings.chunk_size,
						width : resize.width,
						height : resize.height,
						quality : resize.quality,
						multipart : settings.multipart,
						multipart_params : $.extend(settings.multipart_params || {}, file.multipart_params || {}),
						file_data_name : settings.file_data_name,
						format : /\.(jpg|jpeg)$/i.test(file.name) ? 'jpg' : 'png',
						headers : settings.headers,
						urlstream_upload : settings.urlstream_upload
					});
				});


				uploader.bind("Flash:UploadProcess", function(up, flash_file) {
					var file = up.getFile(lookup[flash_file.id]);

					if (file.status != plupload.FAILED) {
						file.loaded = flash_file.loaded;
						file.size = flash_file.size;

						up.trigger('UploadProgress', file);
					}
				});

				uploader.bind("Flash:UploadChunkComplete", function(up, info) {
					var chunkArgs, file = up.getFile(lookup[info.id]);

					chunkArgs = {
						chunk : info.chunk,
						chunks : info.chunks,
						response : info.text
					};

					up.trigger('ChunkUploaded', file, chunkArgs);

					// Stop upload if file is maked as failed
					if (file.status != plupload.FAILED) {
						getFlashObj().uploadNextChunk();
					}

					// Last chunk then dispatch FileUploaded event
					if (info.chunk == info.chunks - 1) {
						file.status = plupload.DONE;

						up.trigger('FileUploaded', file, {
							response : info.text
						});
					}
				});

				uploader.bind("Flash:SelectFiles", function(up, selected_files) {
					var file, i, files = [], id;

					// Add the selected files to the file queue
					for (i = 0; i < selected_files.length; i++) {
						file = selected_files[i];

						// Store away flash ref internally
						id = plupload.guid();
						lookup[id] = file.id;
						lookup[file.id] = id;

						files.push(new plupload.File(id, file.name, file.size, file.thumb_data));
					}

					// Trigger FilesAdded event if we added any
					if (files.length) {
						uploader.trigger("FilesAdded", files);
					}
				});

				uploader.bind("Flash:SecurityError", function(up, err) {
					uploader.trigger('Error', {
						code : plupload.SECURITY_ERROR,
						message : plupload.translate('Security error.'),
						details : err.message,
						file : uploader.getFile(lookup[err.id])
					});
				});

				uploader.bind("Flash:GenericError", function(up, err) {
					uploader.trigger('Error', {
						code : plupload.GENERIC_ERROR,
						message : plupload.translate('Generic error.'),
						details : err.message,
						file : uploader.getFile(lookup[err.id])
					});
				});

				uploader.bind("Flash:IOError", function(up, err) {
					uploader.trigger('Error', {
						code : plupload.IO_ERROR,
						message : plupload.translate('IO error.'),
						details : err.message,
						file : uploader.getFile(lookup[err.id])
					});
				});

				uploader.bind("Flash:ImageError", function(up, err) {
					uploader.trigger('Error', {
						code : parseInt(err.code, 10),
						message : plupload.translate('Image error.'),
						file : uploader.getFile(lookup[err.id])
					});
				});

				uploader.bind('Flash:StageEvent:rollOver', function(up) {
					var browseButton, hoverClass;

					browseButton = document.getElementById(uploader.settings.browse_button);
					hoverClass = up.settings.browse_button_hover;

					if (browseButton && hoverClass) {
						plupload.addClass(browseButton, hoverClass);
					}
				});

				uploader.bind('Flash:StageEvent:rollOut', function(up) {
					var browseButton, hoverClass;

					browseButton = document.getElementById(uploader.settings.browse_button);
					hoverClass = up.settings.browse_button_hover;

					if (browseButton && hoverClass) {
						plupload.removeClass(browseButton, hoverClass);
					}
				});

				uploader.bind('Flash:StageEvent:mouseDown', function(up) {
					var browseButton, activeClass;

					browseButton = document.getElementById(uploader.settings.browse_button);
					activeClass = up.settings.browse_button_active;

					if (browseButton && activeClass) {
						plupload.addClass(browseButton, activeClass);

						// Make sure that browse_button has active state removed from it
						plupload.addEvent(document.body, 'mouseup', function() {
							plupload.removeClass(browseButton, activeClass);
						}, up.id);
					}
				});

				uploader.bind('Flash:StageEvent:mouseUp', function(up) {
					var browseButton, activeClass;

					browseButton = document.getElementById(uploader.settings.browse_button);
					activeClass = up.settings.browse_button_active;

					if (browseButton && activeClass) {
						plupload.removeClass(browseButton, activeClass);
					}
				});

				uploader.bind("QueueChanged", function(up) {
					uploader.refresh();
				});

				uploader.bind("FilesRemoved", function(up, files) {
					var i;

					for (i = 0; i < files.length; i++) {
						getFlashObj().removeFile(lookup[files[i].id]);
					}
				});

				uploader.bind("StateChanged", function(up) {
					uploader.refresh();
				});

				uploader.bind("Refresh", function(up) {
					var browseButton, browsePos, browseSize;

					// Set file filters incase it has been changed dynamically
					getFlashObj().setFileFilters(uploader.settings.filters, uploader.settings.multi_selection);

					// kasparsj
					if (uploader.settings.resize && uploader.settings.resize.beforeUpload)
						getFlashObj().setResizeSettings(uploader.settings.resize);

					if (uploader.settings.thumb)
						getFlashObj().setThumbSettings(uploader.settings.thumb);

					browseButton = document.getElementById(up.settings.browse_button);
					if (browseButton) {
//						browsePos = plupload.getPos(browseButton, document.getElementById(up.settings.container));
						browsePos = {x:0,y:0};
						browseSize = plupload.getSize(browseButton);

						plupload.extend(document.getElementById(up.id + '_flash_container').style, {
							top : browsePos.y + 'px',
							left : browsePos.x + 'px',
							width : browseSize.w + 'px',
							height : browseSize.h + 'px'
						});
					}
				});

				uploader.bind("Destroy", function(up) {
					var flashContainer;

					plupload.removeAllEvents(document.body, up.id);

					delete initialized[up.id];
					delete uploadInstances[up.id];

					flashContainer = document.getElementById(up.id + '_flash_container');
					if (flashContainer) {
						container.removeChild(flashContainer);
					}
				});

				callback({success : true});
			});
		}
	});
})(window, document, plupload);

(function (Just) {
	Just.common = Just.common || {};
	Just.common.uploader = function(settings) {
		var defaults = {
			url: '/ajax/images/save',
			ui: {
				list: {
					tpl: '%control% %list%',
					control: '<div class="{ns}-control">%attach%%upload%%stop%%cancel% %hint%</div>',
					attach : '<div class="attach-wrapper" id="{ns}-placeholder">%placeholder%<input type="button" value="Добавить файлы" id="{id}-attach" class="attach"/></div>',
//					placeholder: '<div style="position: absolute;"><span id="{ns}-placeholder"></span></div>',
					placeholder: '',
					upload : '<a class="upload" href="javascript:void(0);">Загрузить</a>',
					stop : '<a class="stop" href="javascript:void(0);" style="display: none;">Остановить</a>',
					cancel : '<a class="cancelAll" href="javascript:void(0);">Очистить</a>',
					hint : '',
					list: '<ul class="{ns}-list"></ul>'

				},
				file: {
					tpl: '<li id="{file_id}" style="display: none;" data-uploaded="0">%preview%%data%%progress%</li>',
					preview: '<div class="preview" id="{file_id}_preview"><div class="progress"></div><img src="/media/images/blank.gif"/></div>',
//					preview: '',
					progress: '<div class="progress"></div>',
					data: '<div class="data">%name%%size%%alt%%upload%%cancel%</div>',
					name: '<input type="text" disabled="disabled" class="input-type-text name" autocomplete="off" name="name" value="{name}"/>',
//					alt: '<div class="alt">Описание:&nbsp;<input type="text" name="alt"/></div>',
					alt: '<input type="text" class="input-type-text alt" name="alt" placeholder="Описание"/>',
					size: '<div class="size">{size}</div>',
//					cancel: '<div class="cancel"><a href="javascript:void(0);" class="cancel">Отменить</a></div>'
					upload : '<button class="ui-button upload"><div class="sprite-16 sprite-navigation-090-button"></div></button>',
					cancel : '<button class="ui-button cancel"><div class="sprite-16 sprite-cross-button"></div></button>'
//					addalt : '<button class="alt_add" title="Описание"><span class="sprite-16 sprite-keyboard"></span><span>Описание</span></button>',
//					cancel: '<div class="cancel sprite-16 sprite-cross-button"></div><div class="process sprite-16 sprite-navigation-090" style="display: none;"></div>'
				},
				csList: '.just-uploader-list',
				csControl: '.just-uploader-control',
				ns: 'just-uploader'
			},
			gallery: {
				list: {
					tpl: '<div class="{ns}">%list%</div>',
					list: '<ul></ul>'
				},
				item: {
					tpl: '<li style="display: none;">%image%%panel%</li>',
					image: '<a href="{image}" target="_blank" title="{alt}">%alt%%preview%</a>',
					alt: '<div style="display: none;" class="alt">{alt}</div>',
					preview: '<img src="{preview}" alt="image"/>',
					panel: '<div class="panel" data-id="{id}">%panelDelete%</div>',
					panelDelete: '<div class="sprite-16 sprite-cross-button delete" title="Удалить"></div>'
				},
				csList: 'ul',
				csItem: 'li',
				ns: 'just-uploader-gallery',
				delete_url: '/ajax/images/delete',
				panel: {
					hide: false
				}
			},

			autoupload: false,
			// General settings
			runtimes : 'flash',
			max_file_size : '10mb',
//			chunk_size : '1mb',
			multipart : true,
			multipart_params : {"X_REQUESTED_WITH":"XMLHttpRequest"},
			filters : [
				{title : "Image files", extensions : "jpg,gif,png"}
			],
			// Resize images on clientside if we can
			// try setting beforeUpload to true
			resize : {width : 2000, height : 2000, quality : 90, beforeUpload: false},
			// thumbnails
			thumb : {width: 100, height: 100, quality: 90},
			// Flash settings
			flash_swf_url : '/media/js/plupload.swf',
			init: {},
			browse_button: 'just-uploader-placeholder'
		};

		function make_tpl(obj) {
			var tpl = obj.tpl;
			for (var i in obj) {
				tpl = tpl.replace(new RegExp('\%'+i+'\%', 'img'), obj[i]);
			}
			return tpl;
		}

		return Just.$.each(function() {
			var UP, UI, GL, self = this, $self = $(this);
			var target = $self;
			settings = $.extend(true, defaults, settings);
			if (!!settings.autoupload) settings.thumb = false;
			if (settings.debug) settings.debug_handler = function(a) {console.log(a);};
			UI = this.UI = {
				ns: settings.ui.ns,
				settings: settings.ui,
				init: function() {
					target.append(make_tpl(settings.ui.list).replace(/\{ns\}/mg, UI.ns).replace(/\{id\}/mg, target[0].id)).addClass(UI.ns).addClass(UI.ns + '-plupload');
					UI.list = target.find(settings.ui.csList);
					UI.control = target.find(settings.ui.csControl);
					if (!!settings.thumb) {
						UI.list.addClass('thumbs')
					}
					UI.list.find('.data .cancel').live('click', function() {
						UI.cancel($(this).parents('li:first')[0].id);
						return false;
					});
					UI.list.find('.data .upload').live('click', function() {
						var id = $(this).parents('li:first')[0].id, file;
						for (var i in UP.files) {
							file = UP.files[i];
							if (file.id !== id && file.status == plupload.QUEUED) {
								file.status = plupload.WAITING;
							} else if (file.id == id) {
								file.status = plupload.QUEUED;
							}
						}
						UP.start();
						return false;
					});
					UI.list.find('.data .alt_add').live('click', function() {
						$(this).parents('.data:first').find('.alt').slideToggle();
						return false;
					});
					UI.control.find('.cancelAll').bind('click', function() {
						UP.stop();
						UI.list.find('li .cancel').click();
					});
					UI.control.find('.upload').bind('click', function() {
						UP.start();
						UI.list.children('li:first').find('.upload').click();
						UI.control.find('.stop').show();
						UI.control.find('.cancelAll').hide();
					});
					UI.control.find('.stop').bind('click', function() {
						UP.stop();
						UI.control.find('.upload,.cancelAll').show();
						UI.control.find('.stop').hide();
					});
					UI.control.find('.upload,.cancelAll').hide();
				},
				add: function(UP, files) {
					if (!files.length) return;
					for (var i in files) {
						var file = files[i];
						var fs;
						if (file.size < 1024 * 1024) {
							fs = (file.size / 1024).toFixed(2).replace('.', ',') + ' Kb';
						} else {
							fs = (file.size / 1024 / 1024).toFixed(2).replace('.', ',') + ' Mb'
						}
						var str = make_tpl(settings.ui.file).replace(/{ns}/img, UI.ns).replace(/{file_id}/img, file.id).replace(/{name}/img, file.name).replace(/{size}/img, fs);
						UI.list.append(str).find('li:hidden').slideDown(200);
						if (!!settings.autoupload) {
							UI.control.find('.stop,.cancelAll').show();
						} else {
							UI.control.find('.upload,.cancelAll').show();
						}
					}
					if (!!settings.autoupload && files.length) {
//						document.getElementById(UP.id + '_flash').style.visibility = "hidden";
						var interval = setInterval(function() {var l=[]; for (var i in UP.files) {if (UP.files[i].status == plupload.QUEUED) {l.push(i)}}; if (l.length == files.length) {clearInterval(interval); UP.start();}},10);
					}
				},
				attached: function(numFilesSelected, numFilesQueued, queueLen) {
					if (!!settings.autoupload && numFilesQueued > 0) {
//						UP.getMovieElement().style.visibility = "hidden";
						$('#'+UI.ns+'-attach').attr('disabled', true);
						UI.control.find('.upload').click();
					}
				},
				before: function(UP, file) {
					file.multipart_params = {};
					$('#'+file.id).find('input,select,textarea').each(function() {file.multipart_params[this.name] = this.value;});
				},
				start: function(UP, file) {
					UI.control.find('.upload,.cancelAll').hide();
					UI.control.find('.stop').show();
					$('li#'+file.id).find('input,button,textarea').attr('disabled', true);
				},
				cancel: function(id) {
					UP.removeFile(UP.getFile(id));
					$('li#'+id).slideUp(200, function() {
						$(this).remove();
						if (UI.list.children('li').length) {
							UI.control.find('.upload,.cancelAll').show();
							UI.control.find('.stop').hide();
						} else {
							UI.control.find('.upload,.cancelAll,.stop').hide();
						}
					});

				},
				progress: function(UP,file) {
					$('li#'+file.id).find('.progress').css('width', file.percent + '%');
				},
				preview: function(UP, file) {
					var h = $('li#'+file.id+' .preview');
					var img = document.createElement('img');
					img.onload = function() {GL.centrize(img)};
					img.src = 'data:image/jpeg;base64,'+file.thumb_data;
					h.html(img);
				},
				uploaded: function(UP, file, response) {
					response = $.parseJSON(response.response);
					var item = $('li#'+file.id);
					item.data('uploaded', '1');
					if (response.status == 1) {
						item.slideUp(200, function(){$(this).remove()});
					} else {
						item.find('.progress').hide();
						item.find('.process').toggleClass('sprite-navigation-090');
						item.append('<p class="ui-state-error">Ошибка загрузки: '+response.error+'</p>').addClass('error');
						setTimeout(function() { item.slideUp(200, function(){$(this).remove()}); }, 4000);
					}
				},
				complete: function(UP, files) {
					for (var i in UP.files) {
						if (UP.files[i].status == plupload.WAITING) UP.files[i].status = plupload.QUEUED;
					}
					UI.control.find('.stop').hide();
					$('#'+UI.ns+'-attach').attr('disabled', false);
//					if (!!settings.autoupload) {document.getElementById(UP.id+'_flash').style.visibility = "visible";}
					setTimeout(function() {
						if (!UI.list.find('li').length) UI.control.find('.cancelAll').hide();
						else UI.control.find('.cancelAll,.upload').show();
					}, 300);
				},
				state: function (UP, file) {
					if (!!file && !!file.thumb_data && file.thumb_percent == 100 && !file.loaded) {
						UI.preview(UP,file);
					}
				}
			};

			GL = this.Gallery = {
				settings: settings.gallery,
				init: function(UP) {
					target.trigger('GalleryBeforeInit', GL);
					var str = make_tpl(GL.settings.list).replace(/{id}/img, target[0].id).replace(/{ns}/img, GL.settings.ns);
					target.append(str);
					GL.list = target.find('.' + GL.settings.ns).find(GL.settings.csList);
					GL.list.find('li').live({
						mouseover: function() {$(this).find('.alt').show();},
						mouseout: function() {$(this).find('.alt').hide();}
					});
					if (!!GL.settings.panel.hide) {
						GL.list.find('li .panel').hide();
						GL.list.find('li').live({
							mouseover: function() {$(this).children('.panel').show();},
							mouseout: function() {$(this).children('.panel').hide();}
						});
					};
					GL.list.find('li .panel .delete').live({
						click: function() {
							var self = $(this);
							var id = self.parents('.panel:first').data('id');
							$.ajax({
								url: GL.settings.delete_url,
								type: 'POST',
								dataType: 'json',
								data: {id: id},
								success: function(r) {
									if (typeof(r) == 'undefined' || (!r.status && !r.error)) {
										alert('Возникла ошибка при удалении');
									} else if (r.error) {
										alert('Ошибка при удалении: '+r.error);
									} else {
										self.parents('li:first').remove();
									}
								}
							})
						}
					});
					GL.list.find('li .panel>*').live({
						mouseover: function() {$(this).addClass('hover')},
						mouseout: function() {$(this).removeClass('hover')}
					});
					target.trigger('GalleryInit', GL);
				},
				add: function(UP, file, response) {
					response = $.parseJSON(response.response);
					if (response && !response.status) {
//						alert ('Ошибка загрузки файла ' + file.name + ':' + response.error);
					} else if (!!response.filename) {
						var item = make_tpl(GL.settings.item)
								.replace(/{ns}/img, GL.settings.ns)
								.replace(/{id}/img, response.id)
								.replace(/{image}/img, response.image || '')
								.replace(/{preview}/img, response.preview || '')
								.replace(/{filename}/img, response.filename || '')
								.replace(/{(alt|title)}/img, response.alt || '');
						GL.list.append(item).find('li:last').show().find('img').each(function() {var self = this; setTimeout(function() {GL.centrize(self);},100)});
					}
				},
				set: function(files) {
					var tpl = make_tpl(GL.settings.item), str = '';
					if (($.isArray(files) || files instanceof jQuery) === false || !files.length) return;
					$.each(files, function() {
						var item;
						if (this.nodeType) item = $(this).data();
						else item = this;
						str += tpl
								.replace(/{ns}/img, GL.settings.ns)
								.replace(/{id}/img, item.id || '')
								.replace(/{image}/img, item.image || '')
								.replace(/{preview}/img, item.preview || '')
								.replace(/{filename}/img, item.filename || '')
								.replace(/{(alt|title)}/img, item.alt || '');
					});
					(GL.list || this.list).append(str);
					(GL.complete || this.complete)();
				},
				complete: function(up, files) {
					GL.list.find('li').show();
					setTimeout(function() {GL.list.find('li img').each(function() {GL.centrize(this);});}, 210);
				},
				centrize: function(image) {
					if (!image.complete) return $(image).load(function() {GL.centrize(image)});
					var rel = image.width / image.height;
					var parent = $(image).parent();
					if (image.height > image.width) {
						var cw = parent.height() * rel;
						cw = parseInt(parent.width() / 2 - cw / 2).toString() + 'px';
						$(image).css({ 'height': '100%', 'margin-left': cw });
					} else {
						var ch = parent.width() / rel;
						ch = parseInt(parent.height() / 2 - ch / 2).toString() + 'px';
						$(image).css({ 'width': '100%', 'margin-top': ch });
					}
				}
			};

			UI.init();
			UP = self.UP = new plupload.Uploader($.extend({
					dragdrop : true,
					container : 'just-uploader-placeholder'
				}, settings));

			UP.bind('Init', function() {
				GL.init();
			});

			UP.bind('FilesAdded', UI.add);
			UP.bind('BeforeUpload', UI.before);
			UP.bind('UploadFile', UI.start);
			UP.bind('UploadProgress', UI.progress);
			UP.bind('FileUploaded', UI.uploaded);
			UP.bind('FileUploaded', GL.add);
			UP.bind('UploadComplete', UI.complete);
			UP.bind('UploadComplete', GL.complete);
			UP.bind('QueueChanged', UI.state);

			UP.init();
		});
	};

})(Just);
