// JavaScript Document, written and maintained by contact@bernhardkohl.com

Function.prototype.bind = function(obj) { 
	var method = this, 
	tmp = function() { 
 		return method.apply(obj, arguments); 
	};
	return tmp; 
}

/*
###### JS_BASE OBJECT ######
The goal is to provide a simple framework which can be extended by loading additional objects.
	There is one essential extension hard coded into the jBase object, which is BUFFER.
	Whether STATUS is going to be external or not is not yet clear.
*/

var jBase = function() {
	
	// extension objects - created by the extension itself during its initial call
	this.ext = new Object();
	// directory containing the extension files (on the server)
	this.ext.include_path = '/scripts/include/';
	
	// functions
	this.func = new Object();
	
	// global variables
	this.globals = new Object();
	
	// site specific methods etc.
	this.apps = new Object();
	// directory containing the app files (on the server)
	this.apps.include_path = '/scripts/apps/';
	
	// maximum time in seconds until aborting the loading process of an external file
	this.max_loadtime = 30;
	
	/*
	to be called if an extension or app is required (depending on the scope it is called in)
	an extension's or app's id is the same as its filename without '.js'
	e.g. if ( .require('dom') ) { } // --> dom.js
	note that requiring an extension or app does not mean either of them is fully loaded yet since the proces is asynchronous
	an app is initially loaded into the framework as app_[id], since only extensions are allowed to be named after their designated id.
		This allows to name apps without worrying about overriding extensions
	*/
	this.require = function(id, scope) {
		try {
			if (!this.func.is_string(id) || !id.length || !this.func.is_object(this[scope])) return false;
			var global_id = id;
			if (scope !== 'ext') global_id = scope + '_' + id; // only extensions are allowed to be loaded into the framework with their id as name
			if (this.func.is_function(this[global_id])) {
				if (this.func.is_object(this[scope][id])) return true; // already loaded, stop callback
				this[scope][id] = this[scope].loadqueue[id];
				delete this[scope].loadqueue[id];
				if (!this[global_id]()) {
					delete this[scope][id];
					return true; // stop callback
				}
				if (this[scope][id].required) {
					var tmp = new Object();
					tmp.scope = null;
					for (tmp.scope in this[scope][id].required) {
						if (this.func.is_array(this[scope][id].required[tmp.scope]) && !this.func.empty(this[scope][id].required[tmp.scope]) && this.func.is_function(this[tmp.scope].require)) {
							tmp.id = null;
							for (tmp.id in this[scope][id].required[tmp.scope]) {
								this[tmp.scope].require(this[scope][id].required[tmp.scope][tmp.id]);
							}
						}
					}
				}
				return true;
			} else {
				if (!this.func.is_object(this[scope].loadqueue)) this[scope].loadqueue = new Object();
				if (id in this[scope].loadqueue) return false; // <--- async callback
				else { // create callback
					this[scope].loadqueue[id] = this.ext.mk_default_obj();
					this[scope].loadqueue[id].path = this.load_file(id, this[scope].include_path);
					if (!this[scope].loadqueue[id].path) return false;
					if (isNaN(this.max_loadtime) || (this.max_loadtime < 1)) this.max_loadtime = 30;
					this[scope].loadqueue[id].tmp = this.ext.buffer.store([this[scope], 'require'], 'callback_check', 200, Math.round(this.max_loadtime*1000/200), id);
					if (!this[scope].loadqueue[id].tmp) return false;
					return this.ext.buffer.get(this[scope].loadqueue[id].tmp);
				}
			}
			return true;
		} catch(e) { }
		return false;
	}
	this.ext.require = function(id) { return this.require(id, 'ext'); }
	this.ext.require = this.ext.require.bind(this);
	this.apps.require = function(id) { return this.require(id, 'apps'); }
	this.apps.require = this.apps.require.bind(this);
	
	/*
	checks whether some scopes are still in the progress of loading - true if all loaded
	*/
	this.loaded = function() {
		try {
			var tmp;
			if (!this.func) return false;
			for (tmp in this) {
				if (this.func.is_object(this[tmp]) && this[tmp].loadqueue && !this.func.empty(this[tmp].loadqueue)) return false;
			}
			return true;
		} catch(e) { return false; }
	}
	
	/*
	writes to the document to load additional scripts
	id translates to the file's name on the server (without the extension '.js')
	A third parameter can be used to load different file types than .js (so far only .css supported)
	*/
	this.load_file = function(id, dir) {
		try {
			id = '' + id;
			if (!this.func.is_string(id) || !id.length) return false;
			if (this.func.is_function(this[id])) return true; // already loaded
			if (!this.func.is_string(dir)) dir = './'; // default directory is same location as callee script
			if (dir.length && dir.substr && (dir.substr(dir.length - 1) != '/')) dir += '/';
			var tmp = new Object();
			tmp.type = 'js';
			if ((2 in arguments) && this.func.is_string(arguments[2]) && !this.func.empty(arguments[2])) {
				tmp.type = arguments[2] + '';
				tmp.type = tmp.type.toLowerCase();
			}
			tmp.path = dir + id + '.' + tmp.type;
			tmp.err = false;
			tmp.html = '';
			switch (tmp.type) {
				case 'js':
					try {
						tmp.file = document.createElement('script');
						tmp.file.setAttribute('type', 'text/javascript');
						tmp.file.setAttribute('src', tmp.path);
						tmp.html = '<script type="text/javascript" src="' + tmp.path + '"> </script>';
					} catch(e) {
						tmp.err = true;
					}
					break;
				case 'css':
					try {
						tmp.file = document.createElement('link');
						tmp.file.setAttribute('rel', 'stylesheet');
						tmp.file.setAttribute('type', 'text/css');
						tmp.file.setAttribute('href', tmp.path);
						if ((3 in arguments) && (arguments[3] == 'print')) {
							tmp.file.setAttribute('media', arguments[3]);
							tmp.html = ' media="' + arguments[3] + '" '
						}
						tmp.html = '<link type="text/css" rel="stylesheet" href="' + tmp.path + '" media="' + tmp.html + '" />';
					} catch(e) {
						tmp.err = true;
					}
					break;
				default: return false;
			}
			try {
				if (tmp.err) throw false;
				tmp.head = document.getElementsByTagName("head")[0];
				if (!tmp.head.appendChild(tmp.file)) throw false;
				else return tmp.path;
			} catch(e) {
				if (!this.func.empty(tmp.html) && document.write(tmp.html)) return tmp.path;
				else return false;
			}
		} catch(e) { }
		return false;
	}
	
	/*
	Simply creates a default extension/app object and returns it
	.ext.mk_default_obj:
		'path' => points to its include path
		'required' => object of required scopes of required "ids"
	*/
	this.ext.mk_default_obj = function() {
		try {
			var tmp = new Object();
			tmp.path = '';
			tmp.required = new Object();
			tmp.required.ext = new Array();
			return tmp;
		} catch(e) { }
		return false;
	}
	
	/*
	BUFFER EXTENSION
	holds some of the key methods when it comes to asynchronous execution/callback or simply temporary storing of variables etc.
	ext.buffer:
		.storage => stack for the buffer_objects providing several methods
			'length()' => returns the number of buffer_objects currently stored
			'max_id()' => returns the maximum buffer_object_ID currently used
			'get_id()' => returns a free buffer_object_ID
			'mk_obj()' => returns an empty buffer_object
			'objects' => where the buffer_objects are stored (.objects[id], id e [1 ... n])
		
		.store(mixed, [kind, MIXED, MIXED, ...])
			=> returns a positive buffer_object_ID if successful
				if 'kind' is not provided it will be set to its default "value"
				DIFFERENT KINDS:
					--> "value" (DEFAULT) is the simplest form of a buffer object. It's really just a stored value with a global ID.
					
					--> "callback" forms the core of what asynchronous buffer calls are about
						In all callback cases mixed should be a callback function. So it should look like this
							.store(CALLBACK, 'callback')
						Note that it still requires to be called by .get()
					
					--> "callback_timout" triggers buffer_obj.execute() after a certain callback time in milliseconds
						e.g.: .store(CALLBACK, 'callback_timeout', 200) will trigger .get() after 200 milliseconds
									after a timeout of 200 milliseconds CALLBACK will be run by .get()
									Note that it is also possible to supply arguments for CALLBACK using either an array or listing them
									WITH ARGS:
										.store(CALLBACK, 'callback_timeout', 200, Array(ARG1, ARG2, ...)) OR
										.store(CALLBACK, 'callback_timeout', 200, ARG1, ARG2, ...)
			
					--> "callback_check" triggers buffer_obj.execute() repeatedly using a callback time in milliseconds until the return is true or a limit is reached
						e.g.: .store(CALLBACK, 'callback_check', 200, 10)
									after a timeout of 200 milliseconds .get will run CALLBACK
										this will be repeated until CALLBACK returns a TRUE value or the call limit (10 in example) is reached (endless loop if set to NULL)
										Note that the limit of 10 sets the maximum number of calls. So after 10 calls execution will stop.
							WITH ARGS:
								.store(CALLBACK, 'callback_check', 200, null, Array(ARG1, ARG2, ...)) OR
								.store(CALLBACK, 'callback_check', 200, null, ARG1, ARG2, ...))
					
					--> "callback_trigger" triggers buffer_obj.execute() repeatedly until it either returns a true value or a call limit is reached (same as callback_check). If the CALLBACK returns true before the limit its return value is forwarded to a second CALLBACK meaning it triggers another function on succcess.
						e.g.: .store(CALLBACK, 'callback_trigger', 200, 10, CALLBACK2[, ARG1, ARG2, ...])
						
			
			CALLBACK:
				in all supplied examples CALLBACK can either be &FUNCTION or &METHOD	
					&FUNCTION:
						var CALLBACK = function() { return true; }
					&METHOD:
						var obj = new Object();
						obj.method = function() { return true; }
						var CALLBACK = new Array(obj, 'method');
		
		.get(id)
			=> triggers either a callback's execute() or returns the content of a value object - both with ID id
		
		.unset(id)
			=> deletes buffer_object id
			
		.abort(id)
			=> if buffer_object id has a running timeout_pointer this method cancels the timeout
	*/
	this.buffer = function() {
		try {
			if (!this.ext.buffer && !this.func.is_object((this.ext.buffer = this.ext.mk_default_obj()))) return false;
			this.ext.buffer.path = null;
			
			// <--- buffer.storage
			if (!this.ext.buffer.storage) {
				this.ext.buffer.storage = new Object();
				this.ext.buffer.storage.objects = new Object();
			}
			
			this.ext.buffer.storage.length = function() {
				try {
					var count = 0;
					var tmp;
					for (tmp in this.ext.buffer.storage.objects) {
						if (this.func.is_object(this.ext.buffer.storage.objects[tmp])) count++;
					}
					return count;
				} catch(e) { return -1; }
			}
			this.ext.buffer.storage.length = this.ext.buffer.storage.length.bind(this);
			
			this.ext.buffer.storage.max_id = function() {
				try {
					var id = -1;
					var tmp;
					for (tmp in this.ext.buffer.storage.objects) {
						if (this.func.is_object(this.ext.buffer.storage.objects[tmp]) && (tmp > id)) id = tmp;
					}
					return tmp;
				} catch(e) { return -1; }
			}
			this.ext.buffer.storage.max_id = this.ext.buffer.storage.max_id.bind(this);
			
			this.ext.buffer.storage.get_id = function() {
				try {
					var count = this.ext.buffer.storage.length();
					if (count == 0) return 1;
					var id = this.ext.buffer.storage.max_id();
					if (count == id) return ++count;
					else {
						var tmp;
						for (tmp in this.ext.buffer.storage.objects) {
							if (!this.func.is_object(this.ext.buffer.storage.objects[tmp]) && !isNaN(tmp)) return tmp;
						}
					}
					return count++;
				} catch(e) { return -1; }
			}	
			this.ext.buffer.storage.get_id = this.ext.buffer.storage.get_id.bind(this);
			
			this.ext.buffer.storage.mk_obj = function() {
				try {
					var obj = new Object();
					obj.kind = null;
					if (isNaN(obj.id = this.ext.buffer.storage.get_id())) return false;
					return obj;
				} catch(e) { return false; }
			}
			this.ext.buffer.storage.mk_obj = this.ext.buffer.storage.mk_obj.bind(this);
			
			// <--- buffer.store
			this.ext.buffer.store = function(mixed, kind) {
				try {
					var obj;
					if (this.func.empty(mixed) || this.func.empty(obj = this.ext.buffer.storage.mk_obj())) return false;
					if (this.func.is_string(kind)) kind = kind.toLowerCase();
					switch (kind) {
						case 'value':
						case null:
							obj.kind = 'value';
							obj.content = mixed;
							this.ext.buffer.storage.objects[obj.id] = obj;
							return obj.id;
							break;
						case 'callback':
						case 'callback_timeout':
						case 'callback_check':
						case 'callback_trigger':
							obj.kind = kind;
							obj.args = null;
							obj.execute = null;
							obj.callback = null;
							if (this.func.is_array(mixed)) { // &METHOD
								if ((mixed.length !== 2) || !this.func.is_object(mixed[0]) || !this.func.is_string(mixed[1]) || !this.func.is_function(mixed[0][mixed[1]])) return false;
								obj.callback_cb = mixed;
								obj.callback = function() {
									try {
										if (!(this.callback_cb instanceof Array) || (this.callback_cb.length != 2)) return false;
										return this.callback_cb[0][this.callback_cb[1]].apply(this.callback_cb[0][this.callback_cb[1]], arguments);
									} catch(e) { return false; }
								}
								obj.callback = obj.callback.bind(obj);
							} else if (this.func.is_function(mixed)) obj.callback = mixed;
							else return false;
							
							switch (kind) {
								
								// .store(CALLBACK, 'callback'[, ARG1, ARG2, ...])
								case 'callback':
									if (arguments && (arguments.length > 2)) {
										var tmp = new Array();
										for (var count = 2; count < arguments.length; count++) {
											tmp.push(arguments[count]);
										}
										if ((tmp.length == 1) && this.func.is_array(tmp[0])) tmp = tmp[0];
										obj.args = tmp;
									}
									obj.execute = function() {
										try {
											if (this.args) return this.callback.apply(this.callback, this.args);
											else return this.callback();
										} catch(e) { return false; }
									}
									break;
								
								// .store(CALLBACK, 'callback_trigger', t_timeout, call_limit, CALLBACK2[, ARG1, ARG2, ...])
								case 'callback_trigger':
									obj.args_start = 5;
									if (!arguments || !arguments[4]) return false;
									if (this.func.is_function(arguments[4])) obj.callback_target = arguments[4]; // &Function
									else if (this.func.is_array(arguments[4])) { // &METHOD
										if ((arguments[4].length !== 2) || !this.func.is_object(arguments[4][0]) || !this.func.is_string(arguments[4][1]) || !this.func.is_function(arguments[4][0][arguments[4][1]])) return false;
										obj.callback_target_cb = arguments[4];
										obj.callback_target = function(mixed) {
											try {
												if (!(this.callback_target_cb instanceof Array) || (this.callback_target_cb.length != 2)) return false;
												return this.callback_target_cb[0][this.callback_target_cb[1]](mixed);
											} catch(e) { return false; }
										}
										obj.callback_target = obj.callback_target.bind(obj);
									}
									// no break
								
								// .store(CALLBACK, 'callback_check', t_timeout, call_limit[, ARG1, ARG2, ...])
								case 'callback_check':
									if (!obj.args_start) {
										obj.args_start = 4;
										if (!arguments) return false;
									}
									obj.max_calls = 0;
									if (!isNaN(arguments[3]) && (arguments[3] > 0)) obj.max_calls = arguments[3];
									// no break
									
								// .store(CALLBACK, 'callback_timout', t_timeout[, ARG1, ARG2, ...])
								case 'callback_timeout':
									if (!obj.args_start) {
										obj.args_start = 3;
										obj.max_calls = 1;
										if (!arguments) return false;
									}
									if (isNaN(arguments[2])) return false;
									obj.timeout = arguments[2];
									obj.callcount = null;
									if (arguments.length > obj.args_start) {
										var tmp = new Array();
										for (var count = obj.args_start; count < arguments.length; count++) {
											tmp.push(arguments[count]);
										}
										if ((tmp.length == 1) && this.func.is_array(tmp[0])) tmp = tmp[0];
										obj.args = tmp;
									}
									obj.execute = function() {
										try {
											// initial call
											if (this.callcount === null) {
												this.callcount = 0;
												if ((this.timeout_pointer = setTimeout(this.execute, this.timeout))) return true;
												else return false;
											} else {
												this.callcount++;
												var tmp = null;
												if (this.args) tmp = this.callback.apply(this.callback, this.args);
												else tmp = this.callback();
												if (tmp) {
													if (this.callback_target) return this.callback_target(tmp);
													else return tmp;
												} else if (!isNaN(this.max_calls) && ((this.max_calls === 0) || (this.max_calls > this.callcount))) {
													this.timeout_pointer = setTimeout(this.execute, this.timeout);
													return null;
												} else return tmp;
											}
										} catch(e) { return false; }
									}
									break;
							} // end switch 2
							
							obj.execute = obj.execute.bind(obj);
							this.ext.buffer.storage.objects[obj.id] = obj;
							return obj.id;
							break;
						
						default: return -1;
					} // end switch 1
				} catch(e) { }
				return false;
			}
			this.ext.buffer.store = this.ext.buffer.store.bind(this);
			
			// <--- buffer.get
			this.ext.buffer.get = function(id) {
				try {
					if (isNaN(id) || (id < 1) || !this.func.is_object(this.ext.buffer.storage.objects[id])) return false;
					switch (this.ext.buffer.storage.objects[id].kind) {
						case 'value':
							return this.ext.buffer.storage.objects[id].content;
							break;
						case 'callback':
						case 'callback_timeout':
						case 'callback_check':
						case 'callback_trigger':
							return this.ext.buffer.storage.objects[id].execute();
							break;
					}
				} catch(e) { return false; }
			}
			this.ext.buffer.get = this.ext.buffer.get.bind(this);
			
			// <--- buffer.unset
			this.ext.buffer.unset = function(id) {
				try {
					if (isNaN(id) || (id < 1) || !this.func.is_object(this.ext.buffer.storage.objects[id])) return false;
					this.ext.buffer.storage.objects[id] = null;
					return true;
				} catch(e) { return false; }
			}
			this.ext.buffer.unset = this.ext.buffer.unset.bind(this);
			
			// <--- buffer.abort
			this.ext.buffer.abort = function(id) {
				try {
					if (isNaN(id) || (id < 1) || !this.func.is_object(this.ext.buffer.storage.objects[id]) || !this.ext.buffer.storage.objects[id].timeout_pointer) return false;
					return clearTimeout(this.ext.buffer.storage.objects[id].timeout_pointer);
				} catch(e) { return false; }
			}
			this.ext.buffer.abort = this.ext.buffer.abort.bind(this);
			
			return true;
		} catch(e) { return false; }
	} // end ext.buffer
	
	/*
	###### JS BASE FUNCTIONS ###### 
	initially separated from jBase but then included to reduce the framework to a monolithic structure requiring just one variable
	note that methods provided by extensions should be implemented as this.ext.EXTENSION.METHOD()
	*/
	this.func.is_string = function(mixed_var) {
		return (typeof(mixed_var) == 'string');
	}
	
	this.func.is_array = function(mixed_var) {
		return (mixed_var instanceof Array);
	}
	
	this.func.is_int = function(mixed_var) {
		var y = parseInt(mixed_var * 1);
		if (isNaN(y)) return false;
		return mixed_var == y && mixed_var.toString() == y.toString(); 
	}
	
	this.func.is_object = function(mixed_var) {
		if (mixed_var instanceof Array) return false;
		else return ((mixed_var !== null) && (typeof(mixed_var) == 'object'));
	}
	
	this.func.in_array = function(value, arr) { // true/false if value in array?
		if (!this.func.is_array(arr)) return false;
		for (var count = 0; count < arr.length; count++) {
			if (value == arr[count]) return true;
		}
		return false;
	}
	
	this.func.is_function = function(mixed_var) { // true/false if func_ref is a function?
		return (((mixed_var instanceof Function) || (typeof(mixed_var) == 'function')) && (mixed_var !== null));
	}
	
	this.func.empty = function(mixed) { // pretty much the same as its PHP equivalent. Just '0' is false and an empty Object() true
		if ((mixed == null) || (mixed == false) || (mixed == '') || (mixed == 0)) return true;
		else if ((mixed instanceof Array) && !mixed.length) return true;
		else if (typeof(mixed) == 'object') {
			var tmp;
			for (tmp in mixed) {
				return false;
				break;
			}
			return true;
		}
		return false;
	}
	
	try {
		this.buffer();
	} catch(e) { return false; }
}

/* original js_base remainders

	this.func.get_node_name = function(node_obj) { // get_node_name function
		try {
			if (this.func.is_object(node_obj)) {
				if (node_obj.nodeName) return node_obj.nodeName.toLowerCase();
				else if (node_obj.tagName) return node_obj.tagName.toLowerCase();
			}
		} catch(e) {
			// do nothing
		}
		return false;
	}

	this.func.get_node_attribute = function(node_obj, attr) { // get_node_attribute function (id by default)
		try {
			if (this.func.is_object(node_obj)) {
				if (!attr || !this.func.is_string(attr) || !attr.length) attr = 'id';
				var attr_value;
				if (node_obj.getAttribute) {
					attr_value = node_obj.getAttribute(attr.toLowerCase());
					if (!attr_value) attr_value = node_obj.getAttribute(attr.toUpperCase());
				}
				if (node_obj[attr.toLowerCase()] && !attr_value) {
					attr_value = node_obj[attr.toLowerCase()];
				} else if (node_obj[attr.toUpperCase()] && !attr_value) {
					attr_value = node_obj[attr.toUpperCase()];
				}
				if (this.func.is_string(attr_value) && attr_value.length) return attr_value;
			}
		} catch(e) {
			// do nothing
		}
		return false;
	}
*/
