/**
 * $Id::                                                                       $
 * 
 * This file creates the Phase4-namespace for JavaScript-objects.
 * The Framework needs Prototype, so make sure it is loaded.
 * Sme classes in our nacmespace might need scritaculous, so load it ;)
 * 
 * @since 1.1 Supports dynamic loading of classes. This is guaranteed to work
 *            in all following browsers:
 *            - MSIE 6+
 *            - Safari 3+
 *            - FireFox 3+
 *            - Opera 9+
 * 
 * @since 1.2 Dynamic loading works synchonousely, so no need for instance
 *            handlers in Phase4#getInstanceOf() 
 * 
 * @since 1.3 Added Phase4#require() that lets you specify which classes
 *            your own class/subpackage depends on.
 * 
 * @copyright 2008 Phase 4
 * @version 1.3
 * @link $URL::                                                                $
 * @author $Author::                                                           $
 * @package Phase4 
 */

/**
 * The base-object of our namespace.
 * Technically there are no static objects in JS, but this being a
 * variable holding an anonymous object, we can speak of a static class
 * containing static methods & properties.
 * 
 * @static
 */
var Phase4 = {
    /*
	 *  Properties
	 */
	
	/**
	 * The message of the last error, used to store messages between
	 * two Ajax-requests.
	 * 
	 * @since 1.2
	 * @type {String}
	 * @private
	 */
	_error: false,
	
	/**
	 * The version of this piece of art
	 * 
	 * @type {String}
	 */
	Version: '1.3.0',
	
	/*
	 * Exceptions
	 */
	
	/**
	 * Thrown when a class could not be loaded dynamically
	 * 
	 * @type {Exception}
	 */
	UnableToLoadClassException: 'Unable to load #{subpackage}.#{className}: #{status}',
	
	/**
	 * Thrown when a class is requested that is not registered
	 * 
	 * @type {Exception}
	 */
	UnknownClassException: 'Class #{className} is not registered',
	
	/**
	 * Thrown when a subpackage is requested that is not registered
	 * 
	 * @type {Exception}
	 */
	UnknownSubpackageException: 'Subpackage #{subpackage} is not known',
	
	/*
	 * Private methods
	 */
	
	/**
	 * Tries to guess the url to this file
	 * 
	 * @return {String} The path if it could be found, empty string otherwise
	 * @private
	 */
	_getBaseUrl: function() {
		var scripts = $A(document.getElementsByTagName('script')).collect(function(s) { return s.src; });
		var baseUrl = scripts.find(function(src) {
		    return /phase4\.js/.test(src);
		}) || '';
		var uri = location.protocol + '//' + location.host;
		
		//if we have an url, we might need a few steps more
		if (baseUrl.length) {
		    //remove us from url  
			baseUrl = baseUrl.replace(/phase4\.js.*$/, '');
			
			//if url lacks the uri, we rewrite it
			if (baseUrl.indexOf(uri)==-1) {
    			//if url is relative we prepend the current path
    			if (baseUrl.charAt(0)!='/') {
    			    var lastSlashIndex = location.pathname.lastIndexOf('/');
    			    baseUrl = location.pathname.substring(0, lastSlashIndex + 1) + baseUrl;
    			}
    			
    			//finally, we prepend uri
                baseUrl = uri + baseUrl;
            }
		}
		
		//return the url
		return baseUrl;
	},
	
	/**
	 * Returns the filled out exception template as error message
	 * 
	 * @param {String} exceptionTemplate The template we'll use for exceptions
	 * @param {Object} params The parameters that will be filled in template
	 * @return {Error} An error object conatining the requested and filled out message
	 * @private
	 */
	_getException: function(exceptionTemplate, params) {
	    var temp = new Template(exceptionTemplate);
	    return new Error(temp.evaluate(params));
	},
	
	/**
	 * Initializes the namespace
	 * 
	 * @return void
	 * @private
	 */
	_init: function() {
	    //nop
	},
	
	/**
	 * Instantiates a class as specified and returns the instance
	 * 
	 * @throws UnknownSubpackageException if specified subpackage does not exist
	 * @throws UnknownClassException if specified class does not exist 
	 * 
	 * @param {String} subpackage The subpackage in which the class resides
	 * @param {String} className The name of the class that we shall instantiate
	 * @param {Array} args The arguments that shall be passed to constructor
	 * @return {Object} An instance of specified class if instantiation worked, null otherwise
	 * @private
	 */
	_instantiate: function(subpackage, className, args) {
	    //handle possible errors
	    //or create instance
	    if (!Phase4[subpackage]) {
	        throw Phase4._getException(Phase4.UnknownSubpackageException, {subpackage: subpackage});
		} else if (!Phase4[subpackage][className]) {
		    throw Phase4._getException(Phase4.UnknownClassException, {className: className});
		}
		
		//still here, so subclass the requested class and return a new object of subclass
		return new (Class.create(Phase4[subpackage][className], {
			initialize: function($super) {
				$super.apply(this, args);
			}
		}))();
	},
	
	/**
	 * Returns whether a library exists in our namespace
	 * 
	 * @param {String} subpackage The subpackage in which the class resides
	 * @param {String} className The name of the class
	 * @return {bool} True if library exists, false otherwise
	 * @private
	 */
	_isLoaded: function(subpackage, className) {
	    return !!(Phase4[subpackage] && Phase4[subpackage][className]);
	},
	
	/**
	 * Used to late bind classes into namespace.
	 * Tries to load a script-file from our own directory.
	 * Performs a check whether class is already loaded before anything else.
	 * 
	 * Note: We have to return a value so that loading stays in sync. As soon
	 *       as we remove the return value from code, though Ajax is being told
	 *       to run in sync-mode, we go async and get weird error messages.
	 *       My guess is, that the construct 'somevar = somefunc()' creates
	 *       a situation in which the whole process waits for data, thus
	 *       binding the process to the termination of the "childprocess".
	 *       This might be quite interresting to create a sleep() function... 
	 * 
	 * @throws UnableToLoadClassException if neither subpackage nor class can be loaded
	 * @throws Exception if parsing of loaded classfile fails
	 * 
	 * @param {String} subpackage The subpackage in which the specified class resides
	 * @param {String} className The name of the class that shall be loaded
	 * @param {bool} [forceClass] If set to true, will try to load via classname even if subpackage does not exist
	 * @return {bool} True if class could be loaded false otherwise
	 * @private
	 */
	_loadClass: function(subpackage, className, forceClass) {
	    //init vars
	    var ret  = false;
	    var src = Phase4._getBaseUrl();
	    var response;
	    
	    //reset error
		Phase4._error = false;
	    
	    //if class is already present we return
	    //if we couldn't get current path we throw an exception
		if (Phase4._isLoaded(subpackage, className)) {
		    return true;
		} else if (!src)  {
		    throw Phase4._getException(Phase4.UnknownClassException, {
		        subpackage: subpackage,
                className: className,
                'status': 'n/a'
		    });
		}
		
		//if subpackage does not exist and we're not forced to load class
		//we try to load by subpackage-name
		//otherwise we try to load by class-name
		if (!Phase4[subpackage] && !forceClass) {
		    src += subpackage.toLowerCase() + '.js';
		} else {
		    src += className.toLowerCase() + '.js';
		}
		
        //try to load file using AJAX, as this is the only way 
        //to make all browsers happy
		new Ajax.Request(src, {
		    asynchronous: false,      //load asynchronousely so that we can return something
		    evalJS: true,             //what ever comes back, treat it as if it were javascript
		    method: 'get',            //don't change this or we'll get some very weird messages though things work
		    onException: function(request, e) {
		        Phase4._error = e;
		        ret = false;
		    },
		    onFailure: function(transport) {
		        response = transport;
		        ret = false;
		    }.bind(window)
		});
		
		//we might need another run if class isn't loaded yet
		//if we have an error in _error-property, we make sure that class is unregistered and throw the error
		//otherwise we return what is stored in our return value
		if (!Phase4._isLoaded(subpackage, className)) {
		    if (forceClass) {
		        throw Phase4._getException(Phase4.UnableToLoadClassException, {
    		        subpackage: subpackage,
    		        className: className,
    		        'status': (response ? response.getStatus() || '<n/a>' : '<n/a>')
    		    }) + ' from url ' + src;
		    } else {
		        return Phase4._loadClass(subpackage , className, true);
		    }
		} else if (Phase4._error) {
		    if (forceClass && Phase4[subpackage] && Phase4[subpackage][className]) Phase4[subpackage][className] = false;
		    throw Phase4._error;
		} else {
		    return ret;
		}
	},
	
	/* 
	 * Public methods
	 */
	
	/**
	 * Checks whether subpackage.class is registered in our namespace and returns
	 * an instance of it.
	 * Tries to load the class if not present.
	 * 
	 * @throws UnableToLoadClassException if neither subpackage nor class can be loaded
	 * @throws UnknownSubpackageException if specified subpackage does not exist
	 * @throws UnknownClassException if specified class does not exist
	 * @throws Exception if parsing of loaded classfile fails
	 * 
	 * @param {String} subpackage The subpackage that our class resides in
	 * @param {String} className The name of the class to instatiate 
	 * @param {Any} [...] The parameters that will be passed to the classes constructor
	 * @return {Object} An instance of specified class if we could load it, null otherwise
	 */
	getInstanceOf: function(subpackage, className) {
		var args = $A(arguments).slice(2);
		
		//load class and return a new instance
		Phase4._loadClass(subpackage, className);
		return Phase4._instantiate(subpackage, className, args);
	},
		
	/**
	 * Checks whether an exception is of a specific type
	 * 
	 * @param {String} exception The exception that was caught
	 * @param {String} exceptionType The exceptions type that shall be tested against
	 * @return {bool} True if exception is of specified type, false otherwise
	 */
	isExceptionOfType: function(exception, exceptionType) {
		//TODO: implement this
		return false;
	},

    /**
	 * Registers classes into our namespace.
	 * Additionally creates the desired subpackage if it does not already exist.
	 *
	 * @internal We don't know whether Prototype is already loaded, so we use common js
	 * 
	 * @param {String} subpackage The name of the subpackage in which the classes will reside 
	 * @param {Array} classNames The names of the classes that we need to register
	 * @return void
	 * @static
	 */
	registerClasses: function(subpackage, classNames) {
	    //ensure that second parameter is an array
		if (!(classNames instanceof Array)) {
			classNames = new Array(classNames);
		}
		
		//just in case subpackage does not already exist, we create it
		if (!Phase4[subpackage]) Phase4[subpackage] = {};
		
		//great and now let's register those classes
		for (var i=0; i<classNames.length; ++i) {
		    className = classNames[i];
		    Phase4[subpackage][className] = Class.create();
		}
	},

	/**
	 * Registers methods into our namespace.
	 * Additionally creates the desired subpackage if it does not already exist.
	 * 
	 * @internal We don't know whether Prototype is already loaded, so we use common js
	 * 
	 * @param {String} subpackage The name of the subpackage in which the classes will reside 
	 * @param {Array} methodNames The names of the methods that we need to register
	 * @return void
	 * @static
	 */
	registerMethods: function(subpackage, methodNames) {
	    //ensure that second parameter is an array
		if (!(methodNames instanceof Array)) {
			methodNames = new Array(methodNames);
		}
		
		//just in case subpackage does not already exist, we create it
		if (!Phase4[subpackage]) Phase4[subpackage] = {};
		
		//great and now let's register those methods
		for (var i=0; i<methodNames.length; ++i) {
		    methodName = methodNames[i];
		    Phase4[subpackage][methodName] = Prototype.emptyFunction();
		}
	},

    /**
     * Allows subpackage- and class-files to indicate that they require 
     * other subpackages and classes.
     * If argument slack evaluates to true, no exceptions are thrown 
     * 
     * @throws UnableToLoadClassException if neither subpackage nor class can be loaded
	 * @throws UnknownSubpackageException if specified subpackage does not exist
	 * @throws UnknownClassException if specified class does not exist
	 * @throws Exception if parsing of loaded classfile fails
     * 
     * @param {String} subpackage The name of the subpackage in which class resides
     * @param {String} className The name of the class that is required
     * @param {bool} [slack] Indicate that might need this class, but can go on without it
     */
    require: function(subpackage, className, slack) {
        //try to load class
        try {
            Phase4._loadClass(subpackage, className);
        } catch (e) {
            if (!slack) {
                //make sure we throw a real error, not just a string
                if (typeof(e)!='object' || !e.message) e = new Error(e);
                throw e;
            }
        }
    },

	/**
	 * Returns the namespace as newline-separated string.
	 * 
	 * @return {String} The namespace as newline-separated string
	 */
	toString: function() {
		return this.traceNamespace().join('\n');
	},
	
	/**
	 * Traces the namespace using a DFS-algorithm.
	 * 
	 * @return {Array} A sorted array containing all subpackages and classes registered in this namespace
	 */	
	traceNamespace: function() {
		function isPackageOrClass (thingy) {
			/*
			 * if thingy is an object, it is very likely to be a (sub-)package
			 * otherwise it is a class if it is a function and has the function 'addMethods'
			 */
			if (typeof thingy == 'object') {
				return true;
			} else {
				return ((typeof thingy == 'function') && (thingy.addMethods));
			} 
		}
		
		function recurseOnNode(node, prefix) {
			var retArray = new Array();
			
			for (var child in node) {
				if (!isPackageOrClass(node[child])) continue;	//skip entries that are neither package nor class
				if (!child.match(/^[A-Z]{1}/)) continue;		//skip help-classes
				retArray.push(prefix + '.' + child);
				retArray.push(recurseOnNode(node[child], prefix + '.' + child));
			}
			
			return retArray;
		}
		
		return $A(recurseOnNode(Phase4, "Phase4")).flatten().sort();
	}
};

Phase4._init();
