/**
 * @fileoverview Script loader class
 *
 * @author      Marc-Oliver Stühmer
 * @version     1.0.20071206
 */

/**
 * Determine whether an array contains the given value
 *
 * @param   {mixed} value               The value to find
 * @param   {Array} array               The array to search in
 * @return  {Boolean}                   Does the array contain the value?
 */
function inArray(value, array)
{
    for (var i = 0; i < array.length; i++) {
        if (array[i] === value) {
            return true;
        }
    }
    return false;
}


/**
 * @class       Dynamically loads JavaScript files
 * @static
 */
ScriptLoader = {

	/**
	 * Relay the URL parameters to every loaded script
	 * @static
	 * @type	Boolean
	 */
	relayURLParams : true,
    /**
     * Debug mode switch
     * @static
     * @type    Boolean
     */
    debug : false,
    /**
     * Stack of the script URLs to load
     * @private
     * @static
     * @type    Array
     */
    _stack : new Array(),
    /**
     * Array of already loaded scripts
     * @private
     * @static
     * @type    Array
     */
    _loaded : new Array(),
    /**
     * Functions to execute after all scripts have been loaded
     * @private
     * @static
     * @type    Array
     */
    _postLoadFunctions : new Array(),
    /**
     * URLs to load even if already loaded
     * @private
     * @static
     * @type	Array
     */
    _forceURLs : new Array(),
    
    _loadTimes : new Object(),
    _curScriptId : 0,

    /**
     * Load the given JavaScript file(s)
     * @static
     *
     * @param   {mixed} urls            URL(s) of the script(s) to load (Array or String)
     * @param   {Function} [func]     	Function to execute after all scripts have been loaded (<b>optional</b>)
     * @param	{Boolean} [force=false] Load the URL(s) even if already loaded (<b>optional</b>: <i>false</i>)
     * @param	{Boolean} [async=false] Load URL in asynchronous mode (only if single URL given!) (<b>optional</b>: <i>false</i>)
     * @param	{Array} [params]		Parameters to be passed to the function (<b>optional</b>)
     * @throws  {TypeArgumentError}
     */
    load : function (urls, func, force, async, params)
    {
    	force  = force == null ? false : force;
    	async  = async == null ? false : async;
    	params = params == null ? new Array() : params;
    	
        if (typeof urls == 'string') {
            urls = new Array(urls);
        }
        if (!(urls instanceof Array)) {
            throw new TypeArgumentError('\'urls\' parameter must be Array or String');
        }
        
        if (urls.length == 1 && async && ScriptLoader._stack.length == 0) {
        	ScriptLoader.loadAsync(urls[0], func, force, params);
        } else {        
	        if (force) {
	        	ScriptLoader._forceURLs = ScriptLoader._forceURLs.concat(urls);
	        }
	
	        ScriptLoader._stack = urls.concat(ScriptLoader._stack);
	
	        if (func instanceof Function) {
		        var funcobj = {
		        	'func'   : func,
		        	'params' : params
		        };
	            ScriptLoader._postLoadFunctions.unshift(funcobj);
	        } else if (func != null) {
	        	throw new TypeError('Invalid \'func\' parameter given');
	        }

	        if (!ScriptLoader._processingStack) {
	            ScriptLoader._processingStack = true;
	            ScriptLoader._loadNext();
	        }
        }
    },
    
    /**
     * Load the given JavaScript file in asynchronous mode
     * @static
     * 
     * @param	{String} url			URL of the script to load
     * @param	{Function} [func]		Function to execute after the script has been loaded (<b>optional</b>
     * @param	{Boolean} [force=false]	Load the URL even if already loaded (<b>optional</b>: <i>false</i>)
     * @param	{Array} [params]		Parameters to be passed to the function (<b>optional</b>)
     */
    loadAsync : function (url, func, force, params)
    {
    	force  = force == null ? false : force;
    	params = params == null ? new Array() : params; 
    	
        var loaded = inArray(url, ScriptLoader._loaded);

        if (force || !loaded) {
           	if (ScriptLoader.debug) {
            	console.info('loading async: ' + url);
	    	}

        	if (!loaded) { 
        		ScriptLoader._loaded.push(url);
        	}
        	
        	var xmlhttp = createXMLHttpRequest();
			xmlhttp.open('GET', url, true);
			xmlhttp.onreadystatechange = function () {
				if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
		        	if (ScriptLoader.debug) {
		            	console.log('loaded asnyc: ' + url);
		        	}
		        	
					var jscode = xmlhttp.responseText;
					try {
						eval(jscode);
					} catch (e) {
						console.error(e.name + ': ' + e.message + '\n' + xmlhttp.responseText);
					}

					if (func instanceof Function) {
						func.apply(null, params);
					}
				}
			};
			xmlhttp.send(null);
		}
    },

	/**
	 * Add a post load function
     * @static
	 *
	 * @param   {Function} func         Function to add to the post load queue
	 * @param   {Boolean} [atfirst=true] Put the function at the first position? (<b>optional</b>: <i>true</i>)
	 * @param	{Array} [params]		Parameters to be passed to the function (<b>optional</b>)
	 */
	addPostLoadFunction : function (func, atfirst, params)
	{
		atfirst = atfirst == null ? true : atfirst;
		params  = params == null ? new Array() : params; 
		
		var funcobj = {
			'func'   : func,
			'params' : params
		};
		
		if (atfirst) {
			ScriptLoader._postLoadFunctions.unshift(funcobj);
		} else {
			ScriptLoader._postLoadFunctions.push(funcobj);
		}
	},

    /**
     * Load the next JavaScript file from the stack, execute the post load functions
     * @private
     * @static
     */
    _loadNext : function ()
    {
        if (ScriptLoader._stack.length > 0) {
            var url    = ScriptLoader._stack.shift();
            var forced = inArray(url, ScriptLoader._forceURLs);
            var loaded = inArray(url, ScriptLoader._loaded);

            if (url != undefined && (forced || !loaded)) {
               	if (ScriptLoader.debug) {
	            	console.group('loading' + (forced ? ' forced' : '') + ' : ' + url);
    	    	}

				if (forced) {
					removeFromArray(url, ScriptLoader._forceURLs, false);
				}
				
            	if (!loaded) { 
            		ScriptLoader._loaded.push(url);
            	}
            	
            	ScriptLoader._loadScript(url);
			} else {
			    ScriptLoader._loadNext();
			}
        } else if (ScriptLoader._postLoadFunctions.length > 0) {
			var funcobj = ScriptLoader._postLoadFunctions.shift();
			funcobj['func'].apply(null, funcobj['params']);
			ScriptLoader._loadNext();
        } else {
           	ScriptLoader._processingStack = false;
        }
    },

	/**
	 * Load the given JavaScript file
	 * @private
	 * @static
	 * 
	 * @param	{String} url			URL of the script to load
	 */
	_loadScript : function (url)
	{ 
	    var script = document.createElement('script');
	    
        script.onload = ScriptLoader._handleLoad;   // Mozilla, Opera
        // Opera supports both onload and onreadystatechange!
        if (navigator.userAgent.indexOf('Opera') == -1) {
        	script.onreadystatechange = ScriptLoader._handleLoad;   // IE
		}

        script.type = 'text/javascript';
		
		if (ScriptLoader.debug) {
			var date = new Date();
			scriptId = 'script_' + ++ScriptLoader._curScriptId;
			script.setAttribute('id', scriptId);
			ScriptLoader._loadTimes[scriptId] = date.getUTCMilliseconds();
		}
		
        if (ScriptLoader.relayURLParams) {
			var urlparams = window.location.search;
        	if (url.indexOf('?') == -1) {
				script.src = url + urlparams;	            		
        	} else {
        		script.src = url + urlparams.replace(/\?/, '&');  
        	}
        } else {
        	script.src = url;
        }

        document.getElementsByTagName('head')[0].appendChild(script);
	},
	
    /**
     * Handle the 'onload' event of a script, load the next script
     * @private
     * @static
     *
     * @param   {Event} ev              The calling event
     */
    _handleLoad : function (ev)
    {
        // ev is set for Mozilla and Opera, readyState is set for IE
        if (ev || this.readyState == 'loaded' || this.readyState == 'complete') {
        	if (ScriptLoader.debug) {
				var date     = new Date();
				var endTime  = date.getUTCMilliseconds();
				var scriptId = this.getAttribute('id');
				var loadTime = endTime - ScriptLoader._loadTimes[scriptId];
				delete ScriptLoader._loadTimes[scriptId]; 
        		
            	console.info('loaded: ' + this.src + ' (' + loadTime + ' ms)');
            	console.groupEnd();
        	}
            ScriptLoader._loadNext();
		}
    }

};


