/**
 * (c) Copyright Ashantiplc Limited.
 * All Rights Reserved. Duplication prohibited.
 * Redistribution, Transmission, displayed by any means prohibited.
 * You may not alter or remove any trademark, copyright or other notices.
 * Author: Andrey Anisimov
 */

function defined(arg) {
	return (undefined !== arg);
}

function override(oldfunc, newfunc) {
	newfunc.prototype = oldfunc;
	return newfunc;
}

function callback(obj, method) {
	return function() {
		return method.apply(obj, arguments);
	}
}

/**
 * Cross-browser XMLHttp.
 */
var XMLHttpRequestFactory = {

	/**
	 * [public]
	 * Creates and returns cross-browser request object.
	 * 
	 * Return: XMLHttpRequest
	 */
	createRequest : function() {
		if (defined(window.XMLHttpRequest)) {
			return new XMLHttpRequest();
		}
		if (defined(window.ActiveXObject)) {
			try {
				return new ActiveXObject('Msxml2.XMLHTTP');
			} catch (e) {}
			try {
				return new ActiveXObject('Microsoft.XMLHTTP');
			} catch (e) {}
		}
		return null;
	},
	
	toString : function() {
		return '[XMLHttpRequestFactory]';
	}
};

/**
 * Loads files synchronously via HTTP. Please note that if you want
 * to load files asyncronously, you probably need to use core.ajax.sendRequest
 * function instead of HTTPFileLoader.
 */
var HTTPFileLoader = {
	
	/**
	 * Loads XML file and returns DOMDocument object.
	 * 
	 * Param:
	 * path (string) The file path to load.
	 * 
	 * Return: DOMDocument
	 */
	loadXml : function(path) {
		var req = this.requestFile(path);
		if (!defined(req.responseXML)) {
			throw new Error(path + ' is not a valid text/xml file.');
		}
		return req.responseXML.documentElement;
	},
	
	/**
	 * Loads text file and returns its content.
	 * 
	 * Param:
	 * path (string) The file path to load.
	 * 
	 * Return: string
	 */
	loadFile : function(path) {
		var req = this.requestFile(path);
		return req.responseText;
	},
	
	/**
	 * [private]
	 * Creates cross-browser request object and sends the request.
	 * 
	 * Param:
	 * path (string) The file path to load.
	 *
	 * Return: XMLHttpRequest
	 */
	requestFile : function(path) {
		var req = XMLHttpRequestFactory.createRequest();
		try {
			req.open('GET', path, false);
			req.send(null);
			if ((200 == req.status) || (304 == req.status)) {
				return req;
			}
		} catch (e) {}
		throw new Error('Could not find file: ' + path);
	},
	
	toString : function() {
		return '[HTTPFileLoader]';
	}
}

/**
 * Class for getting arguments from script src attribute.
 */
var ExternalScriptConfiguration = {

	/**
	 * [private]
	 * The configuration parameters.
	 * Type: hash table
	 */
	parameters : {},

	/**
	 * [private]
	 * Loads configuration properties from script src attribute.
	 */
	load : function() {
		var scripts = document.getElementsByTagName('script');
		var script = scripts[scripts.length - 1];
		var query = script.src.replace(/^[^\?]+\??/, '');
		var pairs = query.split('&');
		for (var i = 0; i < pairs.length; i++) {
			var pair = pairs[i].split('=');
			var key = unescape(pair[0]);
			var value = unescape(pair[1]);
			ExternalScriptConfiguration.parameters[key] = value.replace(/\+/g, '');
		}
	},

	/**
	 * Returns configuration parameter by its key.
	 *
	 * Param:
	 * key (string) The configuration key.
	 * defaultValue (string) The default value of parameter.
	 */
	getParameter : function(key, defaultValue) {
		return defined(ExternalScriptConfiguration.parameters[key])
			? ExternalScriptConfiguration.parameters[key]
			: defaultValue;
	},

	toString : function() {
		return '[ExternalScriptConfiguration]';
	}
};

ExternalScriptConfiguration.load();

function Package(name) {
	
	this.name = name;
	this.classes = {};
	
	this.toString = function() {
		return '[Package ' + this.name + ']';
	}
}

/**
 * Creator for all package classes.
 * Creates classes objects and processes class definition scripts.
 */
var ClassFactory = {
	
	/**
	 * Creates new class within specified package.
	 * 
	 * Param:
	 * pkg (Package) package object
	 * className (string) - new class name
	 * script (string) [optional] - class definition script
	 */
	createClass : function(pkg, className, script) {
		
		// "Class" is just a function in JavaScript.
		// So, this function should a) append all class-specific methods and properties
		// to the new object, b) append all superclass methods and properties,
		// and c) call the init function (a constructor in Java terminology).

		pkg.classes[className] = false;	// loading flag is on
		
		// Since we can't extend Function or any of the predefined
		// objects, we need the result to be a function.
		pkg[className] = function() {
			this.constructor = arguments.callee;	// Ensure that this.constructor refers to the class function.
			arguments.callee.applyMembers(this);	// Add class properties and methods to `this` object.
			this[arguments.callee.className].apply(this, arguments);	// Call the constructor function.
		}
		
		// Adds all class members to the specified object.
		pkg[className].applyMembers = function(instance) {
			
			var constructorBase;	// Reference to the superclass constructor.
			
			if (this.superClass) {
				// Apply superclass members first.
				this.superClass.applyMembers(instance);
				constructorBase = instance[this.superClass.className];
			}
			
			// Apply all the class members.
			this.constructor.apply(instance);
			
			// Ensure that we have all constructors for all superclasses.
			// This will allow us to use this.BaseClass even if we don't know
			// whether it was defined in the base class or not.
			if ('function' != typeof instance[this.className]) {
				instance[this.className] = function() {
					if ('function' == typeof arguments.callee.prototype) {
						arguments.callee.prototype.apply(this, arguments);
					}
				}
			}
			
			if (constructorBase) {
				// Make base constructor possible to be called as just super()
				instance[this.className] = override(constructorBase, instance[this.className]);
			}
		}
		
		// Returns true if this class is one of subclasses
		// for the specified base class.
		pkg[className].isSubclassOf = function(baseClass) {
			var classFunc = this;
			while (defined(classFunc.superClass)) {
				classFunc = classFunc.superClass;
				if (baseClass == classFunc) return true;
			}
			return false;
		}
		
		var fqClassName = pkg.name + '.' + className;
		
		pkg[className].pkg = pkg;
		pkg[className].className = className;
		pkg[className].toString = new Function("return '[Class " + fqClassName + "]'");
		
		if (defined(script)) {
			try {
				eval("with(" + pkg.name + ") {\n" + ClassFactory.processScript(script, fqClassName) + "\n}");
			} catch (e) {
				throw new Error('Error in class ' + fqClassName + ': [' + e.name + '] ' + e.message);
			}
		}
		
		pkg.classes[className] = true;	// loading flag is off
	},
	
	/**
	 * Creates package object and all its subpackages.
	 * 
	 * Param:
	 * name (string) The fully qualified name of the package.
	 */
	createPackage : function(name) {
		try {
			if (eval(name)) return;
 		} catch (e) {}
		var subpkgs = name.split('.');
		subpkgs.pop();
		if (subpkgs.length > 0) {
			ClassFactory.createPackage(subpkgs.join('.'));
		}
		eval(name + ' = new Package(name)');
		return ClassFactory.createPackage(name);
	},
	
	/**
	 * Replaces all reserved constructions with standart JavaScript.
	 *
	 * Param:
	 * script (string) The class definition script.
	 * fqClassName (string) The fully qualified class name.
	 *
	 * Return: string Replaced script
	 */
	processScript : function(script, fqClassName) {

		// STATEMENT: import package.name;
		// REPLACED WITH: load('package.name');
		script = script.replace(/import\s+([\S]+);/g, "load('$1');");
		
		// STATEMENT: class SubClass extends SuperClass { ... }
		// REPLACED WITH: SubClass.superClass = SuperClass; SubClass.constructor = function()
		script = script.replace(/class\s+(\S+)\s+extends\s+(\S+)\s*\{/, fqClassName + '.superClass = $2; ' + fqClassName + ".constructor = function() {");
		
		// STATEMENT: class NewClass
		// REPLACED WITH: NewClass.constructor = function()
		script = script.replace(/class\s+(\S+)\s*\{/, fqClassName + ".constructor = function() {");
		
		// STATEMENT: super(...);
		// REPLACED WITH: arguments.callee.prototype.apply(this, [ ... ]);
		script = script.replace(/super\s*\((.*)\);/g, "arguments.callee.prototype.apply(this, [ $1 ]);");
		
		// STATEMENT: object instanceof class
		// REPLACED WITH: (object && (object.constructor == class || defined(object.constructor.isSubclassOf) && object.constructor.isSubclassOf(class)))
		script = script.replace(/([\$\.\w]+)\s*instanceof\s*([\$\.\w]+)/g, "($1 && ($1.constructor == $2 || defined($1.constructor.isSubclassOf) && $1.constructor.isSubclassOf($2)))");
		
		return script;
	},

	/**
	 * 
	 * Returns the class by its fully-qualified name.
	 * 
	 * Param:
	 * fqClassName (string) The fully-qualified class name.
	 **/
	getClassByName : function(fqClassName) {
		load(fqClassName);
		return eval(fqClassName);
	},
	
	toString : function() {
		return '[ClassFactory]';
	}
}

/**
 * Package loader class.
 * Loads classes and creates packages automatically.
 */
var PackageLoader = {
	
	/**
	 * [private]
	 * Returns full path of the package descriptor file.
	 *
	 * Param:
	 * name (string) The fully qualified name of the package.
	 *
	 * Return: string
	 */
	getPackageDescriptorPath : function(name) {
		var path = name.replace(/\./g, '/');
		return ExternalScriptConfiguration.getParameter('classpath', 'scripts/') +
			path + '/' + ExternalScriptConfiguration.getParameter('descriptor', '.package');
	},
	
	/**
	 * [private]
	 * Loads class into the existing package.
	 * 
	 * Param:
	 * pkg (Package) package object.
	 * className (string) class name to load
	 */
	loadClass : function(pkg, className) {
		var fqClassName = pkg.name + '.' + className;
		try {
			if (eval(fqClassName)) return;
 		} catch (e) {}
 		var path = fqClassName.replace(/\./g, '/');
		var modulePath = ExternalScriptConfiguration.getParameter('classpath', 'scripts/') + path + '.js';
		try {
			var script = HTTPFileLoader.loadFile(modulePath);
		} catch (e) {
			throw new Error('Class definition file not found: ' + modulePath);
		}
		ClassFactory.createClass(pkg, className, script);
	},
	
	/**
	 * [private]
	 * Loads package data from package descriptor.
	 * 
	 * Param:
	 * packageName (string) The fully qualified package name.
	 */
	loadPackageData : function(packageName) {
		try {
			var path = PackageLoader.getPackageDescriptorPath(packageName);
			var node = HTTPFileLoader.loadXml(path);
		} catch (e) {
			throw new Error(packageName + ' package file not found (searching for ' + path + ')');
		}
		var pkg = eval(packageName + ' = new Package(packageName)');
		var classes = node.getElementsByTagName('class');
		for (var i = 0; i < classes.length; i++) {
			var className = classes.item(i).firstChild.nodeValue;
			pkg.classes[className] = true;
		}
	},
	
	/**
	 * [private]
	 * Creates all intermediate packages and loads package data.
	 * 
	 * Param:
	 * packageName (string) The fully-qualified name of the package to load.
	 */
	loadPackage : function(packageName) {
		try {
			if (eval(packageName)) return eval(packageName);
 		} catch (e) {}
		var subpkgs = packageName.split('.');
		subpkgs.pop();
		if (subpkgs.length > 0) {
			PackageLoader.loadPackage(subpkgs.join('.'));
		}
		PackageLoader.loadPackageData(packageName);
		return PackageLoader.loadPackage(packageName);
	},
	
	/**
	 * [public]
	 * Loads class(es) by the fully-qualified name.
	 * 
	 * Param:
	 * name (string) The fully-qualified class name.
	 */
	load : function(name) {
		var i = name.lastIndexOf('.');
		var className = (-1 == i) ? name : name.substr(i + 1);
		var packageName = (-1 == i) ? '' : name.substr(0, i);
		if (0 == packageName.length) {
			throw new Error('`path` parameter for load() is invalid ("' + name + '" is given)');
		}
		var pkg = PackageLoader.loadPackage(packageName);
		if ('*' == className) {
			for (var cls in pkg.classes) {
				PackageLoader.loadClass(pkg, cls);
			}
		} else if (defined(pkg.classes[className])) {
			PackageLoader.loadClass(pkg, className);
		} else {
			throw new Error('Class ' + name + ' doesn\'t exist. Make sure your package descriptor contains \"' + className + '\" record.');
		}
	},
	
	toString : function() {
		return '[PackageLoader]';
	}
}

/**
 * Loads class by its name. Note that wildcard (*) can be used to load
 * all classes within package, just like in Java.
 * 
 * Param:
 * name (string) The fully-qualified class name.
 * 
 * Throws: Error if load fails.
 */
function load(name) {
	PackageLoader.load(name);
}
