/**
 * Browser data:url: transform binary image data to base64 encoded string right
 * in your browser window.
 *
 * @author Sergey Chikuyonok (serge.che@gmail.com)
 * @link http://chikuyonok.ru
 *
 * @include "lib.js"
 * @include "ZeroClipboard.js"
 */var bdu = (function(){
	var page_anim = {
		transition: 'easeinoutquad',
		time: 0.4
	},

	/** Regexp to match file extensions */
	re_ext = /\.(\w+)$/,

	/** @type {jQuery} File block reference element */
	file_reference = null,

	/** Vendor-specific prefix for CSS properties */
	vendor_prefix = '',

	/** Does current browser supports transitions */
	is_support_transition = false,
	
	/** @type {Element} Special element that will be used in Webkit broswers for text copy */
	copy_clip = null;
	
	

	/**
	 * Returns vendor-specific CSS property name
	 * @param {String} name CSS property name
	 * @return {String}
	 */
	function getVendorProperty(name) {
		return vendor_prefix + name;
	}

	var b64_str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
	/**
	 * Encodes binary data into base64 string
	 * @param {String} input Data stream to encode
	 */
	function base64_encode(input) {
		var output = [];
		var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
		var i = 0, il = input.length;

		while (i < il) {

			chr1 = input.charCodeAt(i++) & 0xff;
			chr2 = input.charCodeAt(i++) & 0xff;
			chr3 = input.charCodeAt(i++) & 0xff;

			enc1 = chr1 >> 2;
			enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
			enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
			enc4 = chr3 & 63;

			if (isNaN(chr2)) {
				enc3 = enc4 = 64;
			} else if (isNaN(chr3)) {
				enc4 = 64;
			}
			
			output.push(
				b64_str.charAt(enc1) + b64_str.charAt(enc2) +
				b64_str.charAt(enc3) + b64_str.charAt(enc4)
			);
		}
		
		return output.join('');
	}

	/**
	 * Collapses content block, showing header in fullscreen
	 * @param {Function} callback Callback function to run after page is collapsed 
	 */
	function collapsePage(callback) {
		var header = $('#header'),
			page = $('#page'),
			height = document.body.offsetHeight;

		$t(page).tween(page_anim, {
			top: height
		});

		$t(header).tween(page_anim, {
			height: $(window).height(),
			onComplete: function() {
				header.css('height', '');
				$('body').removeClass('expanded');
				page.hide();
				if (callback)
					callback();
			}
		});
	}

	/**
	 * Collapses header, showing page content
	 * @param {Function} callback Callback function to run after page is expanded
	 */
	function expandPage(callback) {
		var header = $('#header'),
			page = $('#page'),
			height = $(window).height(),
			anim_obj = {
				transition: 'easeinoutcubic',
				time: 0.6
			};

		$('body').addClass('expanded');

		header.css('height', height);
		$t(header).tween(page_anim, {height: 130});

		page.css({
			display: 'block',
			top: height
		});
		$t(page).tween(page_anim, {
			top: 150,
			onComplete: callback
		});
	}

	/**
	 * Creates file item on page
	 * @param {String} Image file name
	 * @param {String} data_url Base64-encoded image content
	 * @param {Number} [delay] Appearence delay (seconds)
	 */
	function createFile(name, data_url, delay) {
		var file = file_reference.clone(),
			prop = getVendorProperty('transform');
			
		// add file block only when image is loaded and it's dimensions are measured
		var img = new Image;
		img.onload = function(){
			var width = img.width,
				height = img.height;
			
			file
				.data('width', width)
				.data('height', height)
				.find('h3').text(name).end()
				.find('.image img').attr('src', data_url).end()
				.find('.image .size').text(width + '×' + height).end()
				.css(prop, 'scale(0.01)');
	
			$('#content').append(file);
			$t(file).percent({
					transition: 'easeoutcubic',
					time: 0.3,
					delay: delay || 0,
					onComplete: function(){
						file.css(prop, 'none');
					}
				}, function(value){
					file.css(prop, 'scale('+ value +')');
				});
			
			img = null;
		};
		
		img.src = data_url;
	}

	/**
	 * Gracefully removes file from page content,
	 * adjusting sibling element's position
	 * @param {Element|jQuery} file File object to remove
	 */
	function removeFile(file) {
		file = $(file);
		
		removeZeroClipboard(file);

		var siblings = file.nextAll('.file'),
			cur_pos = [],
			next_pos = [],
			// use jTweener namespace to run callback when all siblings are moved
			ns = 'hide_siblings';

		// store current siblings position
		siblings.each(function(i, /* Element */ n){
			cur_pos[i] = {x: n.offsetLeft, y: n.offsetTop};
		});

		// temporay hide current file
		file.hide();

		// store next siblings position
		siblings.each(function(i, /* Element */ n){
			next_pos[i] = {x: n.offsetLeft, y: n.offsetTop};
		});

		file.show();

		var moved_siblings = 0,
			anim_time = 0.5;

		function getDelay() {
			return Math.random() * 0.2 + anim_time / 2;
		}

		/**
		 * Move siblings using browser's build-it transitions for better performance
		 */
		function moveSiblingsTransition() {
			siblings.each(function(i, /* Element */ elem){
				var delta_x = next_pos[i].x - cur_pos[i].x,
					delta_y = next_pos[i].y - cur_pos[i].y,
					prop = getVendorProperty('transform');

				elem = $(elem)
					.css(getVendorProperty('transition'), getVendorProperty('transform') + ' ' + anim_time + 's ease-in-out ' + getDelay() + 's')
					.bind(vendor_prefix.replace(/\-/g, '') + 'TransitionEnd', function(){
						moved_siblings--;
						if (moved_siblings <= 0)
							moveSiblingsComplete();
					});

				moved_siblings++;
				setTimeout(function(){
					elem.css(getVendorProperty('transform'), 'translate(' + delta_x + 'px, ' + delta_y + 'px)');
				}, 1);
			});
		}

		/**
		 * Move siblings using plain-old CSS properties
		 */
		function moveSiblingsPlain() {
			// offset each sibling
			siblings.each(function(i, /* Element */ elem){
				var delta_x = next_pos[i].x - cur_pos[i].x,
					delta_y = next_pos[i].y - cur_pos[i].y;

				elem = $(elem);
				$t(elem).percent({
					transition: 'easeinoutcubic',
					time: anim_time,
					namespace: ns,
					delay: getDelay() // add some chaos to make movement more natural
				}, function(val) {
					elem.css({
						left: delta_x * val,
						top: delta_y * val
					});
				});
			});
		}

		function moveSiblingsComplete() {
			jTweener.removeNSActions(ns);
			file.remove();
			var css_obj = {left: '', top: ''};
			css_obj[getVendorProperty('transform')] = 'none';
			css_obj[getVendorProperty('transition')] = 'none';
			siblings.css(css_obj).unbind(vendor_prefix.replace(/\-/g, '') + 'TransitionEnd');
			
			if (!$('#content .file').length)
				collapsePage();
		}

		// get current file back to view and smoothly hide it
		var transform_prop = getVendorProperty('transform');
		$t(file).percent({
				transition: 'easeoutcubic',
				time: 0.4,
				onComplete: function() {
					if (!siblings.length)
						moveSiblingsComplete();
				}
			}, function(value) {
				value = 1 - value * 0.99;
				file.css(transform_prop, 'scale('+ value +')');
			});

		if (siblings.length) {
			if (is_support_transition) {
				moveSiblingsTransition();
			} else {
				// run callback when on siblings are stopped
				jTweener.addNSAction({onComplete: moveSiblingsComplete}, ns);
				moveSiblingsPlain();
			}
		}
	}

	/**
	 * Stops event propagation and default browser's action
	 * @param {Event} evt
	 */
	function stopEvent(evt) {
		evt.stopPropagation();
		evt.preventDefault();
	}

	/**
	 * Show loaded and encoded files
	 */
	function showFiles(file_list) {
		function run() {
			for (var i = 0; i < file_list.length; i++) {
				createFile(file_list[i].name, file_list[i].data_url, i * 0.1);
			}
		}
		
		if ($('body').hasClass('expanded'))
			run();
		else
			expandPage(run);
	}

	/**
	 * Filters file list and leaves image files only
	 * @param {File[]} files
	 */
	function filterFileList(files) {
		var allowed_types = {png: 1, jpeg: 1, jpg: 1, gif: 1},
			result = [];
			
		for (var i = 0, il = files.length; i < il; i++) {
			var item = (typeof(files[i]) == 'string') ? files[i] : files[i].fileName;
			var m = (item || '').match(re_ext);
			if (m && m[1].toLowerCase() in allowed_types)
				result.push(files[i]);
		}

		return result;
	}

	/**
	 * Drag'n'drop (mainly drop) event handler to read file data in Firefox-way.
	 * @link https://developer.mozilla.org/en/Using_files_from_web_applications
	 * @param {Event} evt
	 */
	function dndFirefox(evt) {
		stopEvent(evt);
		
		var dt = evt.dataTransfer,
			files = dt.files;

		if (dt && files) {
			files = filterFileList(files);
			
			var read_files = [],
				total_files = files.length,
				cur_file = 0;

			function readCallback(evt) {
				read_files.push({
					data_url: evt.target.result,
					name: files[cur_file].name
				});
				
				cur_file++;
				if (cur_file < total_files)
					readNext();
				else
					showFiles(read_files);
			};

			function readNext() {
				var reader = new FileReader();
				reader.onloadend = readCallback;
				reader.readAsDataURL(files[cur_file]);
			}
			
			if (total_files)
				readNext();
		}
	}

	/**
	 * Drag'n'drop (mainly drop) event handler to read file data in generic way 
	 * (Firefox and Webkit).
	 * @param {Event} evt
	 */
	function dndGeneric(evt) {
		stopEvent(evt);
		
		var dt = evt.dataTransfer;
		if (!dt)
			return;
		
		var file_list = '';
		try {
			 file_list = dt.getData('text/plain');
		} catch(e) {}
		
		if (!file_list) {
			try {
				 file_list = dt.getData('text/uri-list');
			} catch(e) {}
		}
		
		if (file_list) {
			file_list = file_list.split(/\r?\n/g);
			
			if (dt.files && file_list.length < dt.files.length) {
				// trick for Google Chrome: in most cases users will drop
				// files from the same folder
				var file_dir = file_list[0].replace(/([\/\\])([^\/\\]+)$/, '$1');
				file_list = [];
				for (var i = 0, il = dt.files.length; i < il; i++) {
					file_list.push(file_dir + dt.files[i].fileName);
				}
			}
			
			
			webkitFileReader(filterFileList(file_list));
		}
	}
	
	/**
	 * Read external files in Webkit-way
	 * @param {String[]} file_list Array of absolute paths to files
	 */
	function webkitFileReader(file_list) {
		var read_files = [],
			total_files = file_list.length,
			data_type_ext = {
				'png': 'image/png',
				'gif': 'image/gif',
				'jpg': 'image/jpeg',
				'jpeg': 'image/jpeg'
			},
			cur_file = 0;

		function readCallback(data) {
			var enc_data = base64_encode(data),
				file_name = file_list[cur_file].match(/[\\\/]([^\\\/]+)$/)[1],
				file_ext = file_name.match(re_ext)[1] || '';

			read_files.push({
				data_url: 'data:' + data_type_ext[file_ext.toLowerCase()] + ';base64,' + enc_data,
				name: file_name
			});

			cur_file++;
			if (cur_file < total_files)
				readNext();
			else
				showFiles(read_files);
		}

		function readNext() {
			$.ajax({
				url: file_list[cur_file],
				xhr: function() {
					var xhr = new XMLHttpRequest();
					// hack to read plain binary stream
					xhr.overrideMimeType('text/plain; charset=x-user-defined');
					return xhr;
				},
				success: readCallback
			});
		}
		
		if (total_files)
			readNext();
	}
	
	/**
	 * Add ZeroClipboard flash movie to file block.
	 * 
	 * If I'll create flash object statically, in createFile(),
	 * your browser will experience heavy performance impact when
	 * many images are opened
	 * @param {jQuery} elem
	 */
	function addZeroClipboard(elem) {
		var data_url = elem.find('.image img').attr('src'),
			width = elem.data('width'),
			height = elem.data('height');
		
		elem.find('dl dd').each(function(i, n){
			var clip = new ZeroClipboard.Client();
			var cl = n.className;
			var copy_data = data_url;
			
			if (cl.indexOf('du-img') != -1)
				copy_data = '<img src="' + data_url + '" width="' + width + '" height="' + height + '" />';
			else if (cl.indexOf('du-bg') != -1)
				copy_data = 'background-image:url(' + data_url + ');';
			
			clip.glue(n);
			clip.setText(copy_data);
			clip.setCSSEffects(false);
		});
	}
	
	/**
	 * Removes ZeroClipboard flash movie from file block
	 * @param {jQuery} elem
	 */
	function removeZeroClipboard(elem) {
		elem.find('.zero-clipboard').remove();
	}
	
	/**
	 * Removes all files from page
	 */
	function removeAll() {
		collapsePage(function(){
			$('#content .file').remove();
		});
	}
	
	function getFlashVersion() {
		try {
			try {
				// avoid fp6 minor version lookup issues
				// see: http://blog.deconcept.com/2006/01/11/getvariable-setvariable-crash-internet-explorer-flash-6/
				var axo = new ActiveXObject('ShockwaveFlash.ShockwaveFlash.6');
				try { axo.AllowScriptAccess = 'always';	} 
				catch(e) { return '6,0,0'; }				
			} catch(e) {}
			return new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version').replace(/\D+/g, ',').match(/^,?(.+),?$/)[1];
		// other browsers
		} catch(e) {
			try {
				if(navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin) {
					return (navigator.plugins["Shockwave Flash 2.0"] || navigator.plugins["Shockwave Flash"]).description.replace(/\D+/g, ",").match(/^,?(.+),?$/)[1];
				}
			} catch(e) {}
		}
		return '0,0,0';
	}
	
	function createCopyClip() {
		copy_clip = document.getElementById('webkit-copy-clip');
	}
	
	function webkitCopy(elem) {
		var file = $(elem).parents('.file:first');
		
		var data_url = file.find('.image img').attr('src'),
			width = file.data('width'),
			height = file.data('height');
			
		var cl = elem.className,
			copy_data = data_url;
		
		if (cl.indexOf('du-img') != -1)
			copy_data = '&lt;img src="' + data_url + '" width="' + width + '" height="' + height + '" /&gt;';
		else if (cl.indexOf('du-bg') != -1)
			copy_data = 'background-image:url(' + data_url + ');';
			
		copy_clip.innerHTML = copy_data;
		copy_clip.focus();
		document.execCommand('copy');
	}
	
	/**
	 * Test if current browser can handle this page
	 */
	function checkSupport() {
		var has_file = !!window.File,
			has_file_reader = window.FileReader,
			page_url = window.location.href,
			msg_box = $('#header .text');
			
		if (!has_file) {
			msg_box.html('Your browser is not supported.<span class="comment">You need Firefox 3.6, Safari 4 or latest Google Chrome (Windows only).</span>')
			return false;
		} else if(!has_file_reader && page_url.indexOf('file://') != 0) {
			msg_box.html('You have to <a href="browser-data-url.zip">download</a> and run this page from your harddrive.');
			return false;
		} else if (getFlashVersion() < 9) {
			msg_box.html('This page requires <a href="http://www.adobe.com/">Adobe Flash</a> 9.0+ player.');
			return false;
		}
		
		return true;
	}

	$(function(){
		file_reference = $('.file').remove();
		
		if (!checkSupport())
			return;
		
		var test_obj = document.body.style;
		// get vendor prefix
		if (typeof(test_obj.webkitTransform) != 'undefined')
			vendor_prefix = '-webkit-';
		if (typeof(test_obj.MozTransform) != 'undefined')
			vendor_prefix = '-moz-';
		if (typeof(test_obj.OTransform) != 'undefined')
			vendor_prefix = '-o-';

		is_support_transition = typeof(test_obj.webkitTransition) != 'undefined';
		
		if (vendor_prefix == '-webkit-') {
			createCopyClip();
		}
		
		var page = $('#page');	
			
		page
			.delegate('.file .close', 'click', function(evt) {
				removeFile($(this).parents('.file:first'));
				evt.cancelBubble = true;
				evt.stopPropagation();
			})
			.delegate('.file img', 'hover', function(/* Event */ evt) {
				var elem = $(this);
				if (evt.type == 'mouseover') { // hover in
					elem.css(getVendorProperty('animation-name'), 'image-show');
				} else { // hover out
					elem.css(getVendorProperty('animation-name'), 'image-hide');
				}
			});
			
		if (copy_clip) { // try to use small Webkit hack to copy data
			page.delegate('.file dd', 'click', function(/* Event */ evt) {
				webkitCopy(this);
			});
		} else { // use flash to copy data
			page.delegate('.file', 'hover', function(/* Event */ evt) {
				var elem = $(this);
				if (evt.type == 'mouseover') { // hover in
					addZeroClipboard(elem);
				} else { // hover out
					removeZeroClipboard(elem);
				}
			});
		}


		$('#header .remove-all').click(removeAll);

		var drop_target = document.body;
		drop_target.addEventListener('dragenter', stopEvent, false);
		drop_target.addEventListener('dragover', stopEvent, false);
		drop_target.addEventListener('drop', (window.FileReader) ? dndFirefox : dndGeneric, false);
		
		var ua = navigator.userAgent.toLowerCase();
		if (ua.indexOf('webkit') != -1 && ua.indexOf('safari') == -1) {
			$('body').addClass('cocoa-app');
		}
	});
	
	return {
		/**
		 * Receive files from external application (Cocoa wrapper, for example)
		 * @param {String} files Line-separated list of files
		 */
		externalDrop: function(files) {
			webkitFileReader(files.split(/\r?\n/g));
		}
	}
})();