var MSGI = {
	page: {}, // FIXME: remove; legacy code uses it instead of MSGI.namspace()

	/**
	 * Create a namespace and return the namespaced object literal.
	 * @param {string} namespace the namespace to create
	 * @returns {object} the namespaced object literal or nil
	 */
	_createNamespace: function(namespace) {
		if (namespace == '') { return nil; }
		var names = namespace.split('.'), obj = window;
		for (var i = 0, len = names.length; i < len; i++) {
			if (typeof obj[names[i]] == 'undefined') { obj[names[i]] = {}; }
			obj = obj[names[i]];
		}
		return obj;
	},

	/** 
	 * Create a MSGI namespace and extend it with and object literal
	 * @param {string} namespace the namespace to create
	 * @param {object} obj (optional) object to extend namespace
	 */
	namespace: function(namespace, obj) {
		var names = (namespace == '') ? [] : namespace.split('.');
		if (names[0] != 'MSGI') { names.unshift('MSGI'); }

		var ns = MSGI._createNamespace(names.join('.'));
		if (obj) { $.extend(ns, obj); }
	},

	/**
	 * Get the value of the specified URL query string param
	 * @param {string} key the name of the query string param to be returned
	 * @param {string} url (optional) the URL to be parsed (defaults to document.location) 
	 * @returns {object} the value of the requested query param (or null if none)
	 */
	getQueryParam: function(key, url) {
		var params = MSGI.getQueryParams(url);
		return params && params[key];
	},

	/**
	 * Convert URL query string to a key/value hash
	 * @param {string} url (optional) the URL to be parsed (defaults to document.location) 
	 * @returns {object} a hash of key/value pairs
	 */
	getQueryParams: function(url) {
		// extract query string from URL, or use native document.location.search if no URL provided
		var query = url ? url.split('?')[1] : document.location.search.slice(1);
		
		if (!query) return {};
		
		// use "&" to split apart sets of key/value pairs
		var pairs = query.split('&');
		
		// populate hash by using "=" to split each key/value pair
		var pair, key, value, params = {};
		for (var i = 0; i < pairs.length; i++) {
			pair = pairs[i].split('=');
			key = pair[0], value = pair[1];
			if (key != '') params[key] = value;
		}
		
		return params;
	},
		
	getChannelSlugFromURL: function() {
		var s = document.location.pathname;	// start with URL path ('/dir/dir/file.html')
		s = s.replace(/^\//, '');			// remove leading forward slash
		s = s.split('/')[0];				// grab everything up to the 1st slash
		s = s.replace(/.+[\.]+.+/, '');		// remove filename (anything with a dot)
		return s || '';
	},

	formatDateString: function(strDate) {
		if(!strDate) return '';
		//date format is 'yyyy-mm-dd hh:mm:ss.ms'
		var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
		var dt = strDate.split(' ');
		var date = dt[0].split('-');
		var time = dt[1].split(':');
		var y = date[0];
		var m = Number(date[1]);
		var d = date[2];
		var h = time[0];
		var min = time[1];
		return (MONTHS[m - 1] + ' ' + d + ', ' + y + ' @ ' + h + ':' + min);
	}
};



/** 
 * Attempt to authenticate the user, redirecting if they cannot be authenticated
 * @param {string} type currently, either "subscriber" or "administrator"
 * @param {function} callback the callback to be fired if the user is authenticated
 */
MSGI.authenticate = (function() {
	function authenticate(type, callback) {
		if (type == 'subscriber') {
			MSGI.subscriber.authenticate(callback);
		} else if (type == 'administrator') {
			MSGI.administrator.authenticate(callback);
		} else {
			redirect();
		}
	}
	
	function saveRedirectURLInCookie(type) {
		var redirectURL = document.location.pathname;
		var query = document.location.search;
		if (query.length) redirectURL += '?' + query.split('?')[1];

		if (type == 'administrator') {
			$.cookie('admin_redirect_url', redirectURL);
		} else {
			$.cookie('redirect_url', redirectURL);
		}
	}
	
	function saveHTTPReferrerInCookie() {
		var referrer = document.referrer;
		if (referrer && referrer.length) {
			$.cookie('redirect_http_referrer', referrer);
		}
	}
	
	function redirect(type) {
		saveRedirectURLInCookie(type);
		saveHTTPReferrerInCookie();
		
		if (type == 'subscriber') {
			document.location = 'login.html';
		} else if (type == 'administrator') {
			document.location = '/manage/';
		} else {
			document.location = '/';
		}
	}
	
	return function(type, callback) {
		authenticate(type, function(authenticated) {
			if (authenticated) {
				callback();
			} else {
				redirect(type);
			}
		});
	};
})();



/** 
 * Fires a callback when a method on a Flash movie is ready to be called.
 * Due to the nature of Flash & JavaScript, this cannot reliably be done synchronously.
 * @param {string} swf the name of the newly created SWF
 * @param {function} callback the callback to be fired when the method can safely be called
 */
MSGI.onFlashReady = function(swf, callback) {
	// define global JS function to be called by Flash when it is ready
	window.flashReady = function() {
		callback(getSWF(swf));
	};
	
	// Flash polls this function until it returns true, at which point it calls flashReady()
	window.isJavascriptReady = function() { 
		return true; 
	};
};



/**
 * A function wrapper to prevent the UI from submitting an Ajax request multiple times 
 * in parallel.  MSGI.lock() fires the callback it is passed, preventing it from firing 
 * again. It then invokes the callback, passing it an "unlock" function it can use to 
 * remove the lock at the appropriate time.
 * @param {function} callback function to be protected (takes an "unlock" function as its argument)
 */
MSGI.lock = (function() {
	// array of string representations of functions that are currently locked
	var lockedFns = [];
	
	return function(callback) {
		// convert callback to string to support locking anonymous functions
		var fn = callback.toString();

		// define some helper functions, bound to the callback
		function index() { return $.inArray(fn, lockedFns); }
		function locked() { return index() > -1; }
		function lock() { lockedFns.push(fn); }
		function unlock() { lockedFns.splice(index(), 1); }
		
		// if not already locked
		if (!locked()) {
			// lock and fire the callback, passing it a reference to its own unlock function
			lock();
			callback(unlock);
		}
	};
})();



(function() {
	function startAnimation(element) {
		element = $(element);
		var height = element.height();
		var rate = 60;
		var frames = 7;
	    var frame = 0;
	
		// use jQuery's data API to get/set state for this spinner element
		function isActive() { return !!element.data('active'); }
		function activate() { element.data('active', true); }
	
		function nextFrame() {
			// cycle frame number from 0 through [frames]
			frame = (++frame > frames) ? 0 : frame;
			return frame;
		}
		
		function animate() {
			if (isActive(element)) {
				// scroll element's BG up to the next frame, and schedule next move
				element.css('background-position', '0 ' + -(height * nextFrame()) + 'px');
				setTimeout(animate, rate); 
			} else {
				// reset element's BG to its initial position
				element.css('background-position', element.data('background-position'));
			}
		}
		
		// make function idempotent (can be called multiple times with no side effects)
		if (isActive()) return;
		
		activate();
		animate();
	}
	
	function stopAnimation(element) {
		$(element).data('active', false);
	}
	
	MSGI.namespace('MSGI.widgets.button', {
		'animate': function(button) {
			var indicator = $(button).find('span:last');
			
			// save the starting BG position of this element (so it can be reset when the animation stops)
			indicator.data('background-position', indicator.css('background-position'));

			// replace the arrow BG image with the spinner, and start animating it
			indicator.removeClass('button-arrow').addClass('activity');
			startAnimation(indicator);
		},

		'stop': function(button) {
			// replace the spinner BG image with the arrow, and stop animating it
			var indicator = $(button).find('span:last');
			indicator.removeClass('activity').addClass('button-arrow');
			stopAnimation(indicator);
		}
	});
})();



/** 
 * Used to run asynchronous processes (eg: Ajax calls) in parallel, and to fire logic when
 * all are completed.  MSGI.inParallel() takes an arbitrary number of functions as arguments:
 *
 *     MSGI.inParallel(fn1, fn2, ..., onComplete)
 *
 * Each function provided to MSGI.inParallel() is passed a callback as an argument.  A 
 * function fires the callback to indicate it has finished, optionally passing data to the 
 * callback.  The last argument provided to MSGI.inParallel() is a callback to be invoked 
 * when *all* previous callbacks have completed.  This last function's arguments are each of 
 * the values provided by the other functions to their callbacks.
 *      
 * An example:
 *
 *     MSGI.inParallel(function(callback) {
 *         callback('abc');
 *     }, function(callback) {
 *         callback('xyz');
 *     }, function(one, two) {
 *         one == 'abc';
 *         two == 'abc';
 *     })
 */
MSGI.inParallel = function() {
	var functions = Array.prototype.slice.call(arguments);
	var onComplete = functions.pop();

	var expected = functions.length;
	var completed = 0;
	var responses = [];

	for (var i = 0; i < expected; i++) {
		(function() {
			var index = i;
			functions[index](function(response) {
				responses[index] = response;
				if (++completed >= expected) {
					onComplete.apply(null, responses);
				}
			});
		})();
	}
}