function Uploader(id, params) {
	this.id = id;
	this.instance = null;

	// normalize params
	if (isNaN(parseInt(params.multiple))) {
		// ensure that maximal file number is greater then zero
		params.multiple = 1;
	}

	params.allowedFilesize = this._normalizeFilesize(params.allowedFilesize);

	// set params to uploader
	this.ready = false;

	this.params = params;
	this._ensureDefaultValues();

	this.files = [];
	this.files_count = 0;
	this.deleted = [];

	// because used outside this class
	this.deleteURL = params.deleteURL;

	this.enableUploadButton();
	this._fixFileExtensions();
	this._attachEventHandler();

	var $me = this;

	if ( this.params.ajax ) {
		$(document).bind('FormManager.WindowManager.Ready', function ($e) {
			$me.init();
		});
	}
	else {
		$(document).ready(function() {
			$me.init();
		});
	}
}

/* ==== Private methods ==== */
Uploader.prototype._fixFileExtensions = function() {
	this.params.allowedFiletypes = this.params.allowedFiletypes.replace(/\*\./g, '').replace(/;/g, ',');
};

Uploader.prototype._attachEventHandler = function() {
	var $me = this;

	$(document).bind('UploadsManager.Uploader.' + crc32(this.id), function ($e, $method) {
		$me[$method].apply($me, Array.prototype.slice.call(arguments, 2));
	});
};

Uploader.prototype._ensureDefaultValues = function() {
	// Upload backend settings

	var $me = this,
		$defaults = {
			baseUrl : '',
			uploadURL : '',
			deleteURL : '',
			previewURL : '',
			allowedFiletypes : '*.*',
			allowedFiletypesDescription : 'All Files',
			allowedFilesize : 0, // Default zero means "unlimited"
			multiple : 0,
			field : '',
			thumb_format: '',
			urls : '',
			names : '',
			sizes : '',
			ajax: false
		};

	$.each($defaults, function ($param_name, $param_value) {
		if ($me.params[$param_name] == null) {
//			console.log('setting default value [', $param_value, '] for missing parameter [', $param_name, '] instead of [', $me.params[$param_name], ']');
			$me.params[$param_name] = $param_value;
		}
	});
};

Uploader.prototype._normalizeFilesize = function($file_size) {
	var $normalize_size = parseInt($file_size);
	if (isNaN($normalize_size)) {
		return $file_size;
	}

	// in kilobytes (flash doesn't recognize numbers, that are longer, then 9 digits)
	return $normalize_size / 1024;
};

Uploader.prototype._prepareFiles = function() {
	var ids = '',
		names = '';

	// process uploaded files
	for (var f = 0; f < this.files.length; f++) {
		if (isset(this.files[f].uploaded) && !isset(this.files[f].temp)) {
			continue;
		}

		ids += this.files[f].id + '|';
		names += this.files[f].name + '|';
	}

	ids = ids.replace(/\|$/, '');
	names = names.replace(/\|$/, '');

	document.getElementById(this.id+'[tmp_ids]').value = ids;
	document.getElementById(this.id+'[tmp_names]').value = names;
	document.getElementById(this.id+'[tmp_deleted]').value = this.deleted.join('|');
};

Uploader.prototype._formatSize = function (bytes) {
	var kb = Math.round(bytes / 1024);

	if (kb < 1024) {
		return kb + ' KB';
	}

	var mb = Math.round(kb / 1024 * 100) / 100;

	return mb + ' MB';
};

/* ==== Public methods ==== */
Uploader.prototype.init = function() {
	var $me = this,
		$uploader_options = {
			runtimes : 'html5,flash,html4',
			chunk_size: '1mb',
			browse_button : this.id + '_browse_button',
			container: this.id + '_container',
			url : this.params.uploadURL,
			flash_swf_url : this.params.baseUrl + '/Moxie.swf',
			multi_selection: this.params.multiple > 1,
			filters : {},
			init: {}
		};

	if ( this.params.allowedFilesize > 0 ) {
		$uploader_options.filters.max_file_size = this.params.allowedFilesize + 'kb';
	}

	if ( this.params.allowedFiletypes != '*' ) {
		$uploader_options.filters.mime_types = [
			{title : this.params.allowedFiletypesDescription, extensions : this.params.allowedFiletypes}
		];
	}

	this.IconPath = this.params.IconPath ? this.params.IconPath : '../admin_templates/img/browser/icons';

	$uploader_options.init['Init'] = function(uploader) {
		$me.onReady();
	};

	$uploader_options.init['FilesAdded'] = function(uploader, files) {
		$.each(files, function (index, file) {
			$me.onFileQueued(file);
		});

		$me.startUpload();
	};

	$uploader_options.init['FilesRemoved'] = function(uploader, files) {
		$.each(files, function (index, file) {
			if ( file.status != plupload.QUEUED ) {
				uploader.stop();
				uploader.start();
			}
		});
	};

	$uploader_options.init['Error'] = function(uploader, error) {
		$me.onError(error);
	};

	$uploader_options.init['BeforeUpload'] = function(uploader, file) {
		return $me.onUploadFileStart(uploader, file);
	};

	$uploader_options.init['UploadProgress'] = function(uploader, file) {
		$me.onUploadProgress(file);
	};

	$uploader_options.init['FileUploaded'] = function(uploader, file, response) {
		$me.onUploadFinished(file, response);
	};

	this.instance = new plupload.Uploader($uploader_options);

	Application.setHook(
		'm:OnAfterFormInit',
		function () {
			$me.renderBrowseButton();
		}
	);

	this.refreshQueue();
};

Uploader.prototype.refreshQueue = function($params) {
	if ( $params !== undefined ) {
		$.extend(true, this.params, $params);

		document.getElementById(this.id+'[upload]').value = this.params.names;
		document.getElementById(this.id+'[order]').value = this.params.names;
	}

	// 1. remove queue DIVs for files, that doesn't exist after upload was made
	var $new_file_ids = this.getFileIds(this.params.names);

	for (var $i = 0; $i < this.files.length; $i++) {
		if ( !in_array(this.files[$i].id, $new_file_ids) ) {
			this.updateQueueFile($i, true);
		}
	}

	this.files = [];
	this.files_count = 0;
	this.deleted = [];

	if (this.params.urls != '') {
		var urls = this.params.urls.split('|'),
			names = this.params.names.split('|'),
			sizes = this.params.sizes.split('|');

		for (var i = 0; i < urls.length; i++) {
			var a_file = {
				// original properties from Uploader
				id : this.getUploadedFileId(names[i]),
				name : names[i],
				size: sizes[i],
				percent: 100,

				// custom properties
				url : urls[i],
				uploaded : 1
			};

			this.files_count++;
			this.files.push(a_file);
		}

		this.updateInfo();
	}
};

Uploader.prototype.getFileIds = function($file_names) {
	var $ret = [];

	if ( !$file_names.length ) {
		return $ret;
	}

	if ( !$.isArray($file_names) ) {
		$file_names = $file_names.split('|');
	}

	for (var i = 0; i < $file_names.length; i++) {
		$ret.push(this.getUploadedFileId($file_names[i]))
	}

	return $ret;
};

Uploader.prototype.getUploadedFileId = function($file_name) {
	return 'uploaded_' + crc32($file_name);
};

Uploader.prototype.enableUploadButton = function() {
	// enable upload button, when plupload runtime is fully loaded
	$('#' + jq(this.id + '_browse_button')).prop('disabled', false).removeClass('button-disabled');
};

Uploader.prototype.renderBrowseButton = function() {
	this.instance.init();
};

Uploader.prototype.remove = function() {
	this.instance.destroy();
};

Uploader.prototype.isImage = function($filename) {
	this.removeTempExtension($filename).match(/\.([^.]*)$/);

	var $ext = RegExp.$1.toLowerCase();

	return $ext.match(/^(bmp|gif|jpg|jpeg|png)$/);
};

Uploader.prototype.getFileIcon = function($filename) {
	this.removeTempExtension($filename).match(/\.([^.]*)$/);

	var $ext = RegExp.$1.toLowerCase(),
		$ext_overrides = {
			'doc': '^(docx|dotx|docm|dotm)$',
			'xls': '^(xlsx|xltx|xlsm|xltm|xlam|xlsb)$',
			'ppt': '^(pptx|potx|ppsx|ppam|pptm|potm|ppsm)$'
		};

	$.each($ext_overrides, function ($new_ext, $expression) {
		var $regexp = new RegExp($expression);

		if ( $ext.match($regexp) ) {
			$ext = $new_ext;

			return false;
		}

		return true;
	});

	var $icon = $ext.match(/^(ai|avi|bmp|cs|dll|doc|dot|exe|fla|gif|htm|html|jpg|js|mdb|mp3|pdf|ppt|rdp|swf|swt|txt|vsd|xls|xml|zip)$/) ? $ext : 'default.icon';

	return this.IconPath + '/' + $icon + '.gif';
};

Uploader.prototype.removeTempExtension = function ($file) {
	return $file.replace(/(_[\d]+)?\.tmp$/, '');
};

Uploader.prototype.getQueueElement = function($file) {
	var $me = this,
		$ret = '',
		$icon_image = this.getFileIcon($file.name),
		$file_label = this.removeTempExtension($file.name) + ' (' + this._formatSize($file.size) + ')',
		$need_preview = false;

	if (isset($file.uploaded)) {
		// add deletion checkbox
		$need_preview = (this.params.thumb_format.length > 0) && this.isImage($file.name);
		$ret += '<div class="left delete-checkbox"><input type="checkbox" class="delete-file-btn" checked/></div>';

		// add icon based on file type
		$ret += '<div class="left">';

		if ($need_preview) {
			$ret += '<a href="' + $file.url + '" target="_new"><img class="thumbnail-image" large_src="' + this.getPreviewUrl($file, true) + '" src="' + $icon_image + '" alt=""/></a>';
		}
		else {
			$ret += '<img src="' + $icon_image + '"/>';
		}

		$ret += '</div>';

		// add filename + preview link
		$ret += '<div class="left file-label"><a href="' + $file.url + '" target="_new">' + $file_label + '</a></div>';
	}
	else {
		// add icon based on file type
		$ret += '<div class="left"><img src="' + $icon_image + '"/></div>';

		// add filename
		$ret += '<div class="left file-label">' + $file_label + '</div>';

		// add empty progress bar
		$ret += '<div id="' + $file.id + '_progress" class="progress-container left"><div class="progress-empty"><div class="progress-full" style="width: 0%;"></div></div></div>';

		// add cancel upload link
		$ret += '<div class="left"><a href="#" class="cancel-upload-btn">Cancel</a></div>';
	}

	$ret += '<div style="clear: both;"/>';
	$ret = $('<div id="' + $file.id + '_queue_row" class="file' + ($need_preview ? ' preview' : '') + '">' + $ret + '</div>');

	// set click events
	$('.delete-file-btn', $ret).click(function ($e) {
		$(this).prop('checked', !$me.deleteFile($file));
	});

	$('.cancel-upload-btn', $ret).click(function ($e) {
		$me.removeFile($file);

		$e.preventDefault();
	});

	// prepare auto-loading preview
	var $image = $('img.thumbnail-image', $ret);

	if ($image.length > 0) {
		var $tmp_image = new Image();
		$tmp_image.src = $image.attr('large_src');

		$($tmp_image).load (
			function ($e) {
				$image.attr('src', $tmp_image.src).addClass('thumbnail');
			}
		);
	}

	return $ret;
};

Uploader.prototype.getSortedFiles = function($ordered_queue) {
	var $me = this;

	var $ret = $.map($me.files, function ($elem, $index) {
		var $file_id = $ordered_queue[$index].replace(/_queue_row$/, ''),
			$file_index = $me.getFileIndex({id: $file_id});

		return $me.files[$file_index].name;
	});

	return $ret;
};

Uploader.prototype.updateQueueFile = function($file_index, $delete_file) {
	var $queue_container = $( jq('#' + this.id + '_queueinfo') );

	if ($delete_file !== undefined && $delete_file) {
		$( jq('#' + this.files[$file_index].id + '_queue_row') ).remove();

		if (this.files.length == 1) {
			$queue_container.css('margin-top', '0px');
		}
		return ;
	}

	var $ret = this.getQueueElement(this.files[$file_index]),
		$row = $(jq('#' + this.files[$file_index].id + '_queue_row'));

	if ($row.length > 0) {
		// file round -> replace
		$row.replaceWith($ret);
	}
	else {
		// file not found - add
		$( jq('#' + this.id + '_queueinfo') ).append($ret);
		$queue_container.css('margin-top', '8px');
	}
};

Uploader.prototype.updateInfo = function($file_index, $prepare_only) {
	if ($prepare_only === undefined || !$prepare_only) {
		if ($file_index === undefined) {
			for (var f = 0; f < this.files.length; f++) {
				this.updateQueueFile(f);
			}
		}
		else {
			this.updateQueueFile($file_index);
		}
	}

	this._prepareFiles();
};

Uploader.prototype.updateProgressOnly = function ($file_index) {
	var $progress_code = '<div class="progress-empty" title="' + this.files[$file_index].percent + '%"><div class="progress-full" style="width: ' + this.files[$file_index].percent + '%;"></div></div>';

	$('#' + this.files[$file_index].id + '_progress').html($progress_code);
};

Uploader.prototype.removeFile = function (file) {
	var count = 0,
		n_files = [],
		$to_delete = [];

	if (!isset(file.uploaded)) {
		this.instance.removeFile(file);
	}

	$.each(this.files, function (f, current_file) {
		if ( current_file.id == file.id || current_file.name == file.name ) {
			$to_delete.push(f);
		}
		else {
			n_files.push(current_file);
			count++;
		}
	});

	for (var $i = 0; $i < $to_delete.length; $i++) {
		this.updateQueueFile($to_delete[$i], true);
	}

	this.files = n_files;
	this.files_count = count;
	this.updateInfo(undefined, true);
};

Uploader.prototype.hasQueue = function() {
	for (var f = 0; f < this.files.length; f++) {
		if (isset(this.files[f].uploaded)) {
			continue;
		}

		return true;
	}

	return false;
};

Uploader.prototype.startUpload = function() {
	if ( this.hasQueue() ) {
		this.instance.start();
	}
};

Uploader.prototype.deleteFile = function(file, confirmed) {
	if (!confirmed && !confirm('Are you sure you want to delete "' + file.name + '" file?')) {
		return false;
	}

	var $me = this;

	$.get(
		this.getDeleteUrl(file),
		function ($data) {
			$me.removeFile(file);
			$me.deleted.push(file.name);
			$me.updateInfo(undefined, true);
		}
	);

	return true;
};

Uploader.prototype.onUploadFileStart = function(uploader, file) {
	var $upload_url = this.params.uploadURL,
		$file_index = this.getFileIndex(file),
		$extra_params = {
			field: this.params.field,
			id: file.id,
			flashsid: this.params.flashsid
		};

	this.files[$file_index].percent = file.percent;
	this.updateProgressOnly($file_index);

	$upload_url += ($upload_url.indexOf('?') ? '&' : '?');

	$.each($extra_params, function ($param_name, $param_value) {
		$upload_url += $param_name + '=' + encodeURIComponent($param_value) + '&';
	});

	uploader.settings.url = $upload_url;

	return true;
};

Uploader.prototype.onUploadProgress = function(file) {
	var $file_index = this.getFileIndex(file);

	this.files[$file_index].percent = file.percent;
	this.updateProgressOnly($file_index);
};

Uploader.prototype.onFileQueued = function(file) {
	if (this.files_count >= this.params.multiple) {
		// new file can exceed allowed file number
		if (this.params.multiple > 1) {
			// it definitely exceed it
			var $error = {
				'file': file, 'code': 'ERROR_1', 'message': 'Files count exceeds allowed limit.'
			};

			this.instance.trigger('Error', $error);
		}
		else {
			// delete file added
			this.files_count++;
			this.files.push(file);

			if (this.files[0].uploaded) {
				this.deleteFile(this.files[0], true);
			}
			else {
				this.instance.removeFile(file);
			}
		}
	}
	else {
		// new file will not exceed allowed file number
		this.files_count++;
		this.files.push(file);
	}

	this.updateInfo(this.files.length - 1);
};

Uploader.prototype.onError = function(error) {
	this.removeFile(error.file);

	if ( error.code == plupload.FILE_SIZE_ERROR ) {
		error.message = 'File size exceeds allowed limit.';
	}
	else if ( error.code == plupload.FILE_EXTENSION_ERROR ) {
		error.message = 'File is not an allowed file type.';
	}

	setTimeout(function () {
		alert('Error: ' + error.message + "\n" + 'Occurred on file ' + error.file.name);
	}, 0);
};

Uploader.prototype.onUploadFinished = function(file, response) {
	var $json_response = eval('(' + response.response + ')');

	if (response.status != 200) {
		return ;
	}

	if ( $json_response.status == 'error' ) {
		var $error = {
			'file': file, 'code': $json_response.error.code, 'message': $json_response.error.message
		};

		this.instance.trigger('Error', $error);

		return ;
	}

	// new uploaded file name returned by OnUploadFile event
	file.name = $json_response.result;

	this.onUploadFileComplete(file);
};

Uploader.prototype.onUploadFileComplete = function(file) {
	// file was uploaded OR file upload was cancelled
	var $file_index = this.getFileIndex(file);

	if ($file_index !== false) {
		// in case if file upload was cancelled, then no info here
		this.files[$file_index].name = file.name;
		this.files[$file_index].percent = file.percent;

		this.files[$file_index].temp = 1;
		this.files[$file_index].uploaded = 1;
		this.files[$file_index].url = this.getPreviewUrl(this.files[$file_index]);
		this.updateInfo($file_index);
	}
};

Uploader.prototype.getPreviewUrl = function($file, $preview) {
	var $url = this.getUrl(this.params.previewURL, $file);

	if ( $preview !== undefined && $preview === true ) {
		$url += '&thumb=1';
	}

	return $url;
};

Uploader.prototype.getDeleteUrl = function($file) {
	return this.getUrl(this.params.deleteURL, $file);
};

Uploader.prototype.getUrl = function($base_url, $file) {
	var $replacements = {
		'#FILE#': $file.name,
		'#FIELD#': this.params.field,
		'#FIELD_ID#': this.id
	};

	var $url = $base_url;

	$.each($replacements, function ($replace_from, $replace_to) {
		$url = $url.replace($replace_from, encodeURIComponent($replace_to));
	});

	if ( $file.temp !== undefined && $file.temp ) {
		$url += '&tmp=1&id=' + $file.id;
	}

	return $url;
};

Uploader.prototype.getFileIndex = function(file) {
	for (var f = 0; f < this.files.length; f++) {
		if (this.files[f].id == file.id) {
			return f;
		}
	}

	return false;
};

Uploader.prototype.onReady = function() {
	this.ready = true;
	UploadsManager.onReady();
};
