/**
 * boot.js
 * 
 * Copyright (c) 2011 Code 42 Software, Inc.
 * 
 * Provides dynamic script/css loading, dependency management and logging - the basic needs for a sizeable web
 * application.
 * 
 * <h2>Logging</h2>
 * 
 * @example log("this is a log message")
 * 
 * <h2>Simple Javascript/CSS loading</h2>
 * 
 * Script and css loading is provided by the functions 'js' and 'css'.
 * 
 * @example js("myscript.js",function() { // callback }), notice only one script allowed
 * @example css("mystylesheet.css"); // no callback
 * 
 * <h2>Dependency management
 * <h2>
 * 
 * Dependency management of multiple scripts is provided by the functions 'dep' and 'ready'
 * 
 * @example dep("myscript.js","my other-script.js",function () { // called after scripts loaded });
 * @example ready(function() { // called after ALL dep scripts loaded });
 * 
 * 
 * @see http://wiki.c42/mediawiki/index.php/Development:boot.js
 * @author Mitch Coopet, mitch@code42.com
 */

//We do this so we avoid errors in IE when a developer fails tries to log to console.
//IE has no console unless debugging tools are open.

var c42 = {
		timers: {
			boot: new Date()
		}
};

(function() {
	try {
		var consoleExists = !!window.console;
		if (!consoleExists) {
			// MUST specify window.console or you get "console is undefined" errors in IE8
			window.console = {};
		}
		if (!console.log) {
			console.log = function() {};
		}
		if (!console.debug) {
			console.debug = console.log;
		}
		if (!console.fine) {
			console.fine = console.debug;
		}
		if (!console.info) {
			console.info = console.log;
		}
		if (!console.warn) {
			console.warn = console.log;
		}
		if (!console.error) {
			console.error = console.log;
		}

		function getLevels(level) {
			var enabled = $log && $log.enabled && $log.consoleExists;
			var error = true; // always true
			var warn = enabled && $log.warn;
			var info = enabled && $log.info;
			var debug = enabled && $log.debug;
			var fine = enabled && $log.fine;

			if (enabled && level) {
				if (level) {
					level = level.toLowerCase();
				}
				var _level = $log.Level;
				warn = (enabled && level != _level.ERROR);
				info = (enabled && (level == _level.INFO || level == _level.DEBUG || level == _level.FINE));
				debug = (enabled && (level == _level.DEBUG || level == _level.FINE));
				fine = (enabled && level == _level.FINE);
			}

			return { error:error, warn:warn, info:info, debug:debug, fine:fine };
		}

		window.$log = {
				consoleExists: consoleExists,
				enabled: true,
				Level: {
					FINE: 'fine',
					DEBUG: 'debug',
					INFO: 'info',
					WARN: 'warn',
					ERROR: 'error'
				},
				error: true,
				warn: false,
				info: false,
				debug: false,
				fine: false,

				/*
				 * Setup logging for the given category with the given level. The category and level are both optional.
				 */
				init: function(category, level) {
					var rv = 'Found ' + category + '. Updated to ' + level;
					var levels = getLevels(level);
					var logger = $log;
					if (category) {
						category = category.toLowerCase();
						if (!$log[category]) {
							var rv = 'Created ' + category + ' with level ' + level;
							$log[category] = {};
						}
						logger = $log[category];
					}
					logger.error = levels.error;
					logger.warn = levels.warn;
					logger.info = levels.info;
					logger.debug = levels.debug;
					logger.fine = levels.fine;

					return rv;
				},

				/**
				 * Set the logging level for the given category and level.
				 * @param category
				 * @param level
				 */
				setLevel: function(category, level, transient) {
					if (category) {
						category = category.toLowerCase();
					}
					if (level) {
						level = level.toLowerCase();
					}
					var rv = $log.init(category, level);

					// set logging level for all child loggers if no category
					if (!category) {
						var levels = getLevels(level);
						for(var c in $log) {
							var o = $log[c];
							if (o.error) {
								o.error = levels.error;
								o.warn = levels.warn;
								o.info = levels.info;
								o.debug = levels.debug;
								o.fine = levels.fine;
								rv = 'All categories updated to ' + level;
							}
						}
					}
					if (!transient) {
						$log.save();
					}
					return rv;
				},

				/**
				 * Show helpful information about how to use the logging.
				 */
				help: function() {
					$log.info && console.info('****************************************************************************************');
					$log.info && console.info('**********  Logging Help  **********'); 
					$log.info && console.info('****************************************************************************************');
					$log.info && console.info('$log.init(category) to register a category in code');
					$log.info && console.info('$log.category.debug && console.debug("My message")');
					$log.info && console.info('$log.reset() to reset logging to defaults and delete cookie');
					$log.info && console.info('$log.save() to save logging to a cookie');
					$log.info && console.info('$log.setLevel(category, level) to set the category to the given level and save in cookie');
					$log.info && console.info('$log.Level.ERROR, $log.Level.WARN, $log.Level.INFO, $log.Level.DEBUG, $log.Level.FINE');
					$log.info && console.info('****************************************************************************************');
				},

				/**
				 * Reset logging levels to defaults and delete cookies.
				 */
				reset: function(loading) {
					if (!loading) {
						// Delete cookie
						document.cookie = 'c42log=; expires=Thu, 01-Jan-1970 00:00:01 GMT';
					}
					var level = (window.buildEnv === 'dev') ? $log.Level.DEBUG : $log.Level.INFO;
					$log.setLevel(null, level, true);
				},

				save: function() {
					if (window.$ && window.$.toJSON && window.$.cookie) {
						var saved = {};
						for(var c in $log) {
							var o = $log[c];
							if (Object.prototype.toString.call(o) !== '[object Function]' && c !== 'consoleExists' && c !== 'Level') {
								saved[c] = o;
							}
						}
						var value = $.toJSON(saved);
						$.cookie('c42log', value, {expires:1});
					}
				},

				load: function() {
					// Load from cookie, if it exists.
					if (window.$ && window.$.evalJSON && window.$.cookie && document.cookie && document.cookie != '') {
						var value = $.cookie('c42log', value, {expires:1});
						if (value) {
							var saved = $.evalJSON(value);
							for(var c in saved) {
								$log[c] = saved[c];
							}
						}
					}
				}
		};

		$log.reset(true);
		$log.init('boot');

	} catch(e) {
		// we don't want logging related logic to cause the app to fail.
		// eat the exception, we can't trust logging
	}
})();

if (!Array.prototype.indexOf) {
	Array.prototype.indexOf = function (obj, start) {
		for (var i = (start || 0); i < this.length; i++) {
			if (this[i] == obj) {
				return i;
			}
		}
		return -1;
	};
}

if (!document.readyState && document.addEventListener) { // for Firefox <= 3.5
	document.readyState = "loading";
	document.addEventListener("DOMContentLoaded", handler = function () {
		document.removeEventListener("DOMContentLoaded", handler, false);
		document.readyState = "complete";
	}, false);
}

(function() {

	var head = document.getElementsByTagName("head")[0], // head reference
	scripts = [], // scripts to load
	pos = 0, // position in scripts
	depQ = [], // dep callbacks
	readyQ = [], // ready callbacks
	finalizing = false,
	loading = false;

	/**
	 * Log a message and optionally any number of objects
	 * 
	 * @param msg String text message.
	 * @param obj Object to be logged with no text so its inspect-able in console.
	 * 
	 * NOTE: IE doesn't handle the log method efficiently
	 */
	function log(msg, obj) {
		if (!$log.enabled || !$log.consoleExists) {
			return;
		}
		// Log the message
		if (arguments.callee.caller && arguments.callee.caller.name) {
			msg = arguments.callee.caller.name + "() " + msg;
		}
		if (typeof console != 'undefined' && console.log) {
			// Chrome, Firebug, Safari, etc
			if (typeof(obj) == "undefined") {
				console.log(msg);
			} else {
				console.log(msg, obj);
			}
		} else if (typeof opera != 'undefined' && opera.postError) {
			// Opera
			if (typeof(obj) == "undefined") {
				opera.postError(msg); 
			} else {
				opera.postError(msg, obj);
			}
		}
		// Log the rest of the objects
		if (arguments.length > 2) {
			for( var i = 2; i < arguments.length; i++ ) {
				if (typeof console != 'undefined' && console.log) {
					console.log(arguments[i]); // Chrome, Firebug, Safari, etc
				} else if (typeof opera != 'undefined' && opera.postError) {
					opera.postError(arguments[i]); // Opera
				}
			}
		}
	}

	/**
	 * Log an error message and optionally any number of objects
	 * 
	 * @param msg String text message.
	 * @param obj Object to be logged with no text so its inspect-able in console.
	 */
	function error(msg, obj) {
		if (!$log.enabled || !$log.consoleExists) {
			return;
		}
		// Log the message
		if (arguments.callee.caller && arguments.callee.caller.name) {
			msg = arguments.callee.caller.name + "() " + msg;
		}
		if (typeof console != 'undefined' && console.error) {
			// Chrome, Firebug, Safari, etc
			if (typeof(obj) == "undefined") {
				console.error(msg);
			} else {
				console.error(msg, obj);
			};
		} else if (typeof console != 'undefined' && console.log) {
			// Chrome, Firebug, Safari, etc
			if (typeof(obj) == "undefined") {
				console.log(msg);
			} else {
				console.log(msg, obj);
			}
		} else if (typeof opera != 'undefined' && opera.postError) {
			// Opera
			if (typeof(obj) == "undefined") {
				opera.postError(msg); 
			} else {
				opera.postError(msg, obj);
			}
		}
		// Log the rest of the objects
		if (arguments.length > 2) {
			for( var i = 2; i < arguments.length; i++ ) {
				if (typeof console != 'undefined' && console.log) {
					console.log(arguments[i]); // Chrome, Firebug, Safari, etc
				} else if (typeof console != 'undefined' && console.error) {
					console.error(arguments[i]); // Chrome, Firebug, Safari, etc
				} else if (typeof opera != 'undefined' && opera.postError) {
					opera.postError(arguments[i]); // Opera
				}
			}
		}
	}

	/**
	 * load dependencies
	 * 
	 * @param scripts -
	 *          any number of script urls to load
	 * @param fn -
	 *          An optional callback to execute after all scripts are loaded
	 */
	function dep() {
		var args = Array.prototype.slice.call(arguments); // arguments is NOT an array, create one
		var fnpos = args.length - 1;
		var fn = isFunc(args[fnpos])?args[fnpos]:null;
		var urls = (null == fn)?args:args.slice(0,fnpos);

		if (fn != null) 
			depQ.unshift(fn);  // always add callback functions to the front

		while (urls.length > 0) {
			var url = urls.shift();
			if (scripts.indexOf(url) == -1) { // NEW script
				scripts.push(url);
			}
		}

		// Kick start the process ONLY ONCE
		if (!loading && !finalizing && scripts.length > 0) {
			loading = true; // set loading state

			function next(url) {
				$log.boot.fine && console.fine("boot.js loading script " + url);

				if (++pos < scripts.length) {
					js(scripts[pos], function(url) {
						next(url);
					});

				} else if (!finalizing) {
					finalizing = true;
					$log.boot.debug && console.debug("All scripts loaded, finalizing callbacks and ready");
					while(depQ.length > 0) depQ.shift()();
					while(readyQ.length > 0) readyQ.shift()();

				} else {
					$log.boot.debug && console.debug("Scripts are loaded, already finalizing - skipping");
				}
			}

			js(scripts[pos], function(url) {
				next(url);
			});
		}
	};

	/**
	 * ready adds a function handler to be invoked after ALL dependencies and callbacks have executed
	 * 
	 * @param fn
	 *          The ready function handler to invoke
	 */
	function ready(fn) {
		readyQ.push(fn);
	};

	/**
	 * css - load a css file
	 * 
	 * @param href -
	 *          the href attribute of the link tag to create
	 * @param media -
	 *          the media type of the css file
	 */
	function css(href, media)  {
		var node = document.createElement("link");
		node.href = href;
		node.media = media?media:'screen';
		node.type = 'text/css';
		node.rel = 'stylesheet';

		// log("loading css " + href);
		head.appendChild(node); 
	};

	/**
	 * js - load a single javascript file
	 * 
	 * @param url
	 *          The url to the script
	 * @param callback
	 *          The callback to invoke
	 */
	function js(url, cb)  {

		var s = document.createElement('script');
		s.type = 'text/javascript';
		s.src = url;
		s.async = false; // try to load synchronously

		// Some browsers have no way to detect if a script 404'd.
		// This timeout is a fail-safe to ensure that scripts still load
		// if one of them is unable to be retrieved.
		var timeoutId = setTimeout(function() {
			error('Timed out while waiting for: ' + url);
			cb(url);
		}, 30000);

		s.onreadystatechange = s.onload = function() {
			var state = s.readyState;
			if (isFunc(cb) && !cb.done && (!state || /loaded|complete/.test(state))) {
				clearTimeout(timeoutId);
				cb.done = true;	
				cb(url);
				$log.boot.fine && console.fine("boot.js loaded script " + url);
			}
		}; 

		head.appendChild(s); 
	};

	// Private helper functions
	function isFunc(el) {
		return Object.prototype.toString.call(el) == '[object Function]';
	} 

	/**
	 * attaches browser + platform classes to html tag, borrowed from css_browser_selector :
	 * https://github.com/rafaelp/css_browser_selector/blob/master/css_browser_selector.js
	 */
	function applyPlatformClass(u){
		var ua = u.toLowerCase(),
		is = function(t) {
			return ua.indexOf(t) > -1;
		},
		g = 'gecko',
		w = 'webkit',
		s = 'safari',
		o = 'opera',
		m = 'mobile',
		h = document.documentElement,
		b = [(!(/opera|webtv/i.test(ua)) && /msie\s(\d)/.test(ua)) ? ('ie ie' + RegExp.$1) : is('firefox/2') ? g + ' ff2' : is('firefox/3.5') ? g + ' ff3 ff3_5' : is('firefox/3.6') ? g + ' ff3 ff3_6' : is('firefox/3') ? g + ' ff3' : is('gecko/') ? g : is('opera') ? o + (/version\/(\d+)/.test(ua) ? ' ' + o + RegExp.$1 : (/opera(\s|\/)(\d+)/.test(ua) ? ' ' + o + RegExp.$2 : '')) : is('konqueror') ? 'konqueror' : is('blackberry') ? m + ' blackberry' : is('android') ? m + ' android' : is('chrome') ? w + ' chrome' : is('iron') ? w + ' iron' : is('applewebkit/') ? w + ' ' + s + (/version\/(\d+)/.test(ua) ? ' ' + s + RegExp.$1 : '') : is('mozilla/') ? g : '', is('j2me') ? m + ' j2me' : is('iphone') ? m + ' iphone' : is('ipod') ? m + ' ipod' : is('ipad') ? m + ' ipad' : is('mac') ? 'mac' : is('darwin') ? 'mac' : is('webtv') ? 'webtv' : is('win') ? 'win' + (is('windows nt 6.0') ? ' vista' : '') : is('freebsd') ? 'freebsd' : (is('x11') || is('linux')) ? 'linux' : '', 'js'],
		c = b.join(' ');
		h.className += ' ' + c;
		return c;
	}

	/**
	 * Allows you to run different code in production and development.
	 * If the production code is run, dep() will be replaced with a dummy function that
	 * calls back immediately. The callback will be passed a reference to the real dep so
	 * that it can always load scripts. 
	 * 
	 * Defaults to dev if server is in dev mode.
	 * Override with #devMode=true and #devMode=false
	 */
	function devModeSwitch(devCallback, prodCallback) {
		var devModeHashMatch = window.location.hash.match(/[#|&]devMode=([^&]*)/);
		var devMode = devModeHashMatch ? devModeHashMatch[1] != 'false' : window.buildEnv ? window.buildEnv == 'dev' : true;
		// XXX: Using combined files breaks several pages in Firefox 3.6. For some reason the document ready/loaded events never get fired.
		devMode = (/ff3_6/.test(platform)) ? true : devMode;
		var realDep = window.dep;
		
		if (!devMode) {
			window.dep = function() {
				var fnpos = arguments.length - 1;
				var fn = isFunc(arguments[fnpos])?arguments[fnpos]:null;
				if (fn !== null) 
					fn(); 
			};
			
			window.ready = function(cb) {
				cb();
			};
			
			prodCallback(realDep);
		} else {
			devCallback(realDep);
		}
	}

	// Export api functions
	window.log = log;
	window.error = error;
	window.js = js; 
	window.css = css;  
	window.dep = dep; 
	window.ready = ready; 
	window.platform = applyPlatformClass(navigator.userAgent);
	window.devModeSwitch = devModeSwitch;

	ready($log.load);

})();



