/**
 * Ajax Component lookup method
 */
function ajaxComponent(selector, context, allowRecursive) {
	var results = [];
	var i = 0;
	var recursive = false;
	if (allowRecursive && allowRecursive === true) {
		recursive = true;
	}
	
	jQuery(selector, context).each(
		function(index, domElement) {
			var divId = domElement.id;
			if (divId == '') {
				divId = '_ajaxComponent' + ajaxComponent._objectIdIndex;
				ajaxComponent._objectIdIndex++;
			}
			if (ajaxComponent._objectMap[divId]) {
				results[i] = ajaxComponent._objectMap[divId];
				i++;
			}
			else {
				var theObject = ajaxComponent._createObject(divId, domElement);
				if (theObject) {
					//Recursively add children
					if (recursive === true) {
						var subComponents = ajaxComponent._traverseSubComponents(theObject.getDom());
						for (var j in subComponents) {
							subComponents[j]._setParent(theObject);
							theObject._addToChildrenList(subComponents[j].getId(), subComponents[j]);
						}
					}
					
					theObject.initialize();
					ajaxComponent._objectMap[divId] = theObject;
					results[i] = theObject;
					i++;
				}
			}
		}
	);
	
	if (i == 0) {
		return undefined;
	}
	else if (i == 1) {
		return results[0];
	}
	else {
		return results;
	}
}

/**
 * Create new Ajax Component without DOM
 */
ajaxComponent.create = function(id, className) {
	var obj = null;
	if (id && id !== '') {
		obj = ajaxComponent('#' + id);
	}
	else {
		id = '_ajaxComponent' + ajaxComponent._objectIdIndex;
		ajaxComponent._objectIdIndex++;
	}
	if (obj) {
		return obj;
	}
	else {
		var evalObject = undefined;
		try {
			evalObject = eval("new " + className + "('" + id + "')");
		}
		catch(e){}
		if (evalObject && evalObject instanceof BaseAjaxComponent) {
			obj = evalObject;
			obj._setClassName(className);
			obj.initialize();
			ajaxComponent._objectMap[id] = obj;
		}
		return obj;
	}
}

/**
 * Ajax Component remove method
 */
ajaxComponent.remove = function(obj) {
	var divId = null;
	if (typeof obj === 'string') {
		divId = obj;
	}
	else if (obj instanceof BaseAjaxComponent) {
		divId = obj.getId();
	}
	if (ajaxComponent._objectMap[divId]) {
		ajaxComponent._objectMap[divId].removeAllChildren();
		ajaxComponent._objectMap[divId].finalize();
		delete(ajaxComponent._objectMap[divId]);
	}
}

ajaxComponent._objectMap = {};

ajaxComponent._objectIdIndex = 0;

ajaxComponent._createObject = function(divId, domElement) {
	var theObject = undefined;
	var classStr = domElement.className;
	if (classStr) {
		jQuery.each(classStr.split(' '), function(index, item){
			var className = item.replace(/^\s+|\s+$/g,'');
			if (className != '') {
				var evalObject = undefined;
				try {
					evalObject = eval("new " + className + "('" + divId + "')");
				}
				catch(e){}
				if (evalObject && evalObject instanceof BaseAjaxComponent) {
					theObject = evalObject;
					theObject._setClassName(className);
					domElement.id = divId;
					theObject.dom = domElement;
				}
			}
		});
	}
	return theObject;
}

ajaxComponent._traverseSubComponents = function(parent) {
	var results = [];
	var j = 0;
	var children = parent.childNodes;
	if (children) {
		for (var i in children) {
			var cObject = undefined;
			if (children[i].id !== '') {
				cObject = ajaxComponent('#' + children[i].id, parent, true);
				if (cObject) {
					results[j] = cObject;
					j++;
				}
			}
			if (cObject === undefined) {
				var subRessults = ajaxComponent._traverseSubComponents(children[i]);
				for (var k in subRessults) {
					results[j] = subRessults[k];
					j++;
				}
			}
		}
	}
	return results;
}

ajaxComponent._callAjaxActionWrapper = function(component, handlerName, actionName, actionForm, persistenceId, execNextCallback) {
	component.ajaxCallStarted();
	callAjaxAction(handlerName, actionName, actionForm, true, 
		function(componentResults) {
			if (execNextCallback) {
				execNextCallback(true);
			}
			component.ajaxCallFinished();
			component._dispatchAjaxResult(componentResults);
		},
		function(errorString, exception) {
			if (execNextCallback) {
				execNextCallback(false);
			}
			component.ajaxCallFinished();
			component.userError(errorString, exception);
		}, 
		function(validationErrors) {
			if (execNextCallback) {
				execNextCallback(false);
			}
			component.ajaxCallFinished();
			component.validationError(validationErrors);
		},
		function(errorString, exception) {
			if (execNextCallback) {
				execNextCallback(false);
			}
			component.ajaxCallFinished();
			component.systemError(errorString, exception);
		}, 
		component.timeout,
		persistenceId
	);
}

/**
 * Base class for Bell.ca Ajax Component
 */
function BaseAjaxComponent(id){
	
	this.id = id;
	
	this.dom = null;
	
	this.parent = null;
	
	this.children = {};
	
	// Set default web service timeout for 5sec.
	if (this.timeout === undefined) {
		this.timeout = 5000;
	}
	
	// Specify the AJAX handler name
	if (this.ajaxHandlerName === undefined) {
		this.ajaxHandlerName = null;
	}
	
	// Enable Ajax queue or not
	if (this.queueAjax === undefined) {
		this.queueAjax = true;
	}
	
	// If the AJAX queue is enable, Specify milliseconds to delay while
	// the call is triggered before an actual AJAX call is made to prevent rapid AJAX calls
	if (this.defaultRespondSpeed === undefined) {
		this.defaultRespondSpeed = 1000;
	}
	
	// Set the path of the cookie. Unset or null to the path of the current page
	if (this.cookiePath === undefined) {
		this.cookiePath = null;
	}
	
	// Set cookie expires in days
	if (this.cookieExpires === undefined) {
		this.cookieExpires = 90;
	}
	
	// Set key of the persistence ID in cookie
	if (this.persistenceKey === undefined) {
		this.persistenceKey = null;
	}
}
BaseAjaxComponent.prototype = {
	/**
	 * Get ID of the component
	 */
	getId: function() {
		return this.id;
	},
	
	/**
	 * Get class name of the component
	 */
	getClassName: function() {
		return this.className;
	},
	
	/**
	 * Get jQuery object for this component
	 */
	getJQuery: function() {
		if (this.parent != null) {
			return jQuery('#' + this.id, this.parent.getDom());
		}
		else {
			return jQuery('#' + this.id);
		}
	},
	
	/**
	 * Get DOM object for this component
	 */
	getDom: function() {
		if (this.dom == null) {
			if (this.getJQuery().get(0) != null) {
				this.dom = this.getJQuery().get(0);
			}
			else {
				this.dom = document.createElement('div');
				this.dom.id = this.id;
				this.dom.className = this.className;
			}
		}
		
		return this.dom;
	},
	
	/**
	 * Set DOM object for this component
	 */
	setDom: function(dom) {
		// Only allow set DOM before it is binded into the document
		if (!this.domBinded()) {
			this.dom = dom;
			this.dom.id = this.id;
			if (!this.dom.className) {
				this.dom.className = this.className;
			} else if (this.dom.className.indexOf(this.className) == -1) {
				var newClassName = this.dom.className;
				newClassName += " ";
				newClassName += this.className;
				this.dom.className = newClassName;
			}
		}
	},
	
	/**
	 * Returns DOM of this component binded in the document
	 */
	domBinded: function() {
		return (this.getJQuery().get(0) != null);
	},
	
	
	/**
	 * Add child component into this component
	 */
	addChild: function(child) {
		if (child) {
			child._setParent(this);
			this._addToChildrenList(child.getId(), child);
		}
	},
	
	/**
	 * Remove a child component from this component
	 */
	removeChild: function(obj) {
		var id = null;
		if (typeof obj === 'string') {
			id = obj;
		}
		else {
			id = obj.id;
		}
		if (children[id]) {
			ajaxComponent.remove(id);
			delete(children[id]);
		}
	},
	
	/**
	 * Remove all children components from this component
	 */
	removeAllChildren: function() {
		for (var cId in children) {
			ajaxComponent.remove(children[cId]);
		}
		children = {};
	},
	
	/**
	 * Get all children components as an array
	 */
	getChildren: function() {
		return this.children;
	},
	
	/**
	 * Get parent component of this component
	 */
	getParent: function() {
		return this.parent;
	},
	
	/**
	 * Call Ajax action
	 */
	callAjaxAction: function(actionName, actionForm, callGroup, respondSpeed) {
		if (!this.ajaxHandlerName) {
			this.ajaxHandlerName = this.className + 'Handler';
		}
		this.callAjaxActionWithHandlerName(this.ajaxHandlerName, actionName, actionForm, callGroup, respondSpeed);
	},
	
	/**
	 * Call Ajax action with explicit handler name
	 */
	callAjaxActionWithHandlerName: function(handlerName, actionName, actionForm, callGroup, respondSpeed) {
		if (!actionForm) {
			actionForm = {};
		}
		actionForm.componentId = this.id;
		actionForm.componentClassName = this.className;
		actionForm.actionName = actionName;
		
		var persistenceId = null;
		if (this.persistenceKey != null) {
			persistenceId = this.getCookie(this.persistenceKey);
		}
		
		if (this.queueAjax === false) {
			ajaxComponent._callAjaxActionWrapper(this, handlerName, actionName, actionForm, persistenceId);
		}
		else {
			if (!callGroup) {
				callGroup = handlerName + '_' + actionName;
			}
			if (respondSpeed === undefined) {
				respondSpeed = this.defaultRespondSpeed;
			}
			ajaxComponent.ajaxQueue.call(this, handlerName, actionName, actionForm, persistenceId, callGroup, respondSpeed);
		}
	},
	
	/**
	 * Clone this object to another one
	 */
	clone: function(id) {
		//Don't allow recursively clone the component
		for (var i in this.children) {return null;}
		
		// If object has been binded to the document, just returns existing one
		var obj = ajaxComponent('#' + id);
		if (obj) {
			return obj;
		}
		else {
			obj = jQuery.extend(true, {}, this);
			if (obj) {
				obj.id = id;
				obj.setDom(this.getDom().cloneNode(true));
				obj.initialize();
				ajaxComponent._objectMap[id] = obj;
			}
			return obj;
		}
	},
	
	/**
	 * Set a cookie to store information in the client browser
	 * The value can be a string or a JSON object
	 * Caution: Don't set huge JSON object in the cookie. Some browsers only accept 4KB cookies in total
	 */
	setCookie: function(name, value) {
		var option = {path:this.cookiePath, expires:this.cookieExpires};
		ajaxComponent.util.cookie(name, value, option);
	},
	
	/**
	 * Get the value in the client cookie
	 */
	getCookie: function(name) {
		return ajaxComponent.util.cookie(name);
	},
	
	/**
	 * Callback method while the component object is initialized
	 * To be overriden 
	 */
	initialize: function() {
	},
	
	/**
	 * Callback method while the component object is discarded
	 * To be overriden 
	 */
	finalize: function() {
	},
	
	/**
	 * Initialize DOM with callback data from AJAX
	 * Only works with new component creation triggered from the server side
	 * To be overriden 
	 */
	initializeDom: function(data, html) {
	},
	
	/**
	 * Callback method to update component front end
	 * To be overriden 
	 */
	update: function(data, html, newComponents) {
	},
	
	/**
	 * Callback method to handle Ajax user error
	 * To be overriden 
	 */
	userError: function(errorString, exception) {
		alert("User error: " + errorString);
	},
	
	/**
	 * Callback method to handle form validation error
	 * To be overriden 
	 */
	validationError: function(validationErrors) {
		var message = "Validation error: \n";
		for (var p in validationErrors) {
			message += p + ": " + validationErrors[p] + "\n";
		}
		alert(message);
	},
	
	/**
	 * Callback method to handle Ajax system error
	 * To be overriden 
	 */
	systemError: function(errorString) {
		alert("System error: " + errorString);
	},
	
	/**
	 * Callback method when Ajax action call started
	 * To be overriden 
	 */
	ajaxCallStarted: function() {
	},
	
	/**
	 * Callback method when Ajax action call finished
	 * To be overriden 
	 */
	ajaxCallFinished: function() {
	},
	
	_setClassName: function(className) {
		this.className = className;
	},
	
	_setParent: function(parentComponent) {
		this.parent = parentComponent;
	},
	
	_addToChildrenList: function(id, obj) {
		this.children[id] = obj;
	},
	
	_dispatchAjaxResult: function(componentResults) {
		if (componentResults.resultType == 0) {
			var responses = componentResults.componentResponses;
			if (responses) {
				for (var i in responses) {
					var components = ajaxComponent(responses[i].selector);
					if (components) {
						if (components.constructor.toString().indexOf('Array()') != -1) {
							for (var j in components) {
								this._processComponentUpdate(components[j], responses[i]);
							}
						}
						else {
							this._processComponentUpdate(components, responses[i]);
						}
					}
				}
			}
		}
		else if  (componentResults.resultType == 1) {
			window.location.href = componentResults.uri;
		}
		
		if (this.persistenceKey && componentResults.persistenceId) {
			this.setCookie(this.persistenceKey, componentResults.persistenceId);
		}
	},
	
	_processComponentUpdate: function(component, ajaxResponse) {
		var newComponents = null;
		var nc = ajaxResponse.newComponents;
		var j = 0;
		if (nc) {
			for (var i in nc) {
				var obj = ajaxComponent.create(nc[i].id, nc[i].className);
				if (obj) {
					if (newComponents == null) {
						newComponents = [];
					}
					obj.initializeDom(nc[i].data, nc[i].htmlContent);
					component.addChild(obj);
					newComponents[j] = obj;
					j++;
				}
			}
		}
		component.update(ajaxResponse.data, ajaxResponse.htmlContent, newComponents);
	}
}

/**
 * Returns I18N message from the bundle defined in the messageBunlde tag
 */
ajaxComponent.i18n = function(bundleName, bundleKey, args) {
	var bundle = eval('ajaxComponentI18n.' + bundleName);
	if (bundle) {
		var value = bundle[bundleKey];
		if (value && args) {
			for (var i in args) {
				var token = '{' + i + '}';
				value = value.replace(token, args[i]);
			}
		}
		return value;
	}
	else {
		return null;
	}
}

/**
 * Queue AJAX calls
 **/
ajaxComponent.ajaxQueue = {};
ajaxComponent.ajaxQueue._dequeuing = false;
ajaxComponent.ajaxQueue._callTimers = {};

ajaxComponent.ajaxQueue._ajaxCallTimeoutListener = null;
ajaxComponent.ajaxQueue._ajaxCallTimeoutValue = 0;
ajaxComponent.ajaxQueue._ajaxCallTimeoutTimer = null;
ajaxComponent.ajaxQueue._ajaxCallEndListener = null;

/**
 * Call AJAX with queue
 */
ajaxComponent.ajaxQueue.call = function (component, handlerName, actionName, actionForm, persistenceId, callGroup, respondSpeed) {
	if (ajaxComponent.ajaxQueue._callTimers[callGroup]) {
		clearTimeout(ajaxComponent.ajaxQueue._callTimers[callGroup]);
	}
	ajaxComponent.ajaxQueue._callTimers[callGroup] = setTimeout(function() {
			delete(ajaxComponent.ajaxQueue._callTimers[callGroup]);
			jQuery.queue(ajaxComponent.ajaxQueue, "ajaxCalls", function() {
				ajaxComponent._callAjaxActionWrapper(component, handlerName, actionName, actionForm, persistenceId,
					function(continueQueue) {
						// If callback decides to terminate the queue, clear the queue
						if (continueQueue !== undefined && continueQueue === false) {
							jQuery.queue(ajaxComponent.ajaxQueue, "ajaxCalls", []);
						}
						// If queue is empty
						if (jQuery.queue(ajaxComponent.ajaxQueue, "ajaxCalls").length === 0) {
							ajaxComponent.ajaxQueue._dequeuing = false;
							// Set to run CallEndListener
							if (ajaxComponent.ajaxQueue._ajaxCallTimeoutTimer) {
								clearTimeout(ajaxComponent.ajaxQueue._ajaxCallTimeoutTimer);
								ajaxComponent.ajaxQueue._ajaxCallTimeoutTimer = null;
							}
							if (ajaxComponent.ajaxQueue._ajaxCallEndListener) {
								ajaxComponent.ajaxQueue._ajaxCallEndListener();
							}
						}
						else {
							// Continue dequeuing
							jQuery.dequeue(ajaxComponent.ajaxQueue, "ajaxCalls");
						}
					}
				);
			});
			// If the dequeuing is not started
			if (ajaxComponent.ajaxQueue._dequeuing === false) {
				ajaxComponent.ajaxQueue._dequeuing = true;
				// Set to run CallTimeoutListener
				if (ajaxComponent.ajaxQueue._ajaxCallTimeoutListener) {
						ajaxComponent.ajaxQueue._ajaxCallTimeoutTimer = setTimeout(function() {
								ajaxComponent.ajaxQueue._ajaxCallTimeoutListener();
								ajaxComponent.ajaxQueue._ajaxCallTimeoutTimer = null;
							}, 
							ajaxComponent.ajaxQueue._ajaxCallTimeoutValue);
				}
				// Start dequeuing
				jQuery.dequeue(ajaxComponent.ajaxQueue, "ajaxCalls");
			}
		}, respondSpeed);
}

/**
 * Set a global listner to call back when AJAX calls started for certain milliseconds
 */
ajaxComponent.ajaxQueue.setCallTimeoutListener = function(listener, timeoutValue) {
	ajaxComponent.ajaxQueue._ajaxCallTimeoutListener = listener;
	if (timeoutValue === undefined) {
		timeoutValue = 0;
	}
	ajaxComponent.ajaxQueue._ajaxCallTimeoutValue = timeoutValue;
}

/**
 * Set a global listner to call back when AJAX calls ended
 */
ajaxComponent.ajaxQueue.setCallEndListener = function(listener) {
	ajaxComponent.ajaxQueue._ajaxCallEndListener = listener;
}

ajaxComponent.util = {};

/**
 * Cookie Utility method
 */
ajaxComponent.util.cookie = function(name, value, options) {
    if (typeof value != 'undefined') {
        options = options || {};
        if (value === null) {
            value = '';
            options = $.extend({}, options);
            options.expires = -1;
        }
        var expires = '';
        if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
            var date;
            if (typeof options.expires == 'number') {
                date = new Date();
                date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
            } else {
                date = options.expires;
            }
            expires = '; expires=' + date.toUTCString();
        }
        var path = options.path ? '; path=' + (options.path) : '';
        var domain = options.domain ? '; domain=' + (options.domain) : '';
        var secure = options.secure ? '; secure' : '';
        var cookieStr = '';
        if (typeof value === 'string') {
        	cookieStr = value;
        }
        else {
        	cookieStr = JSON.stringify(value);
        }
        document.cookie = [name, '=', encodeURIComponent(cookieStr), expires, path, domain, secure].join('');
    } else {
        var cookieValue = null;
        if (document.cookie && document.cookie != '') {
            var cookies = document.cookie.split(';');
            for (var i = 0; i < cookies.length; i++) {
                var cookie = jQuery.trim(cookies[i]);
                if (cookie.substring(0, name.length + 1) == (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        if (cookieValue != null && ajaxComponent.util.startWith(cookieValue, '{') 
        		&& ajaxComponent.util.endWith(cookieValue, '}')) {
        	cookieValue = JSON.parse(cookieValue);
        }
        return cookieValue;
    }
};

/**
 * String utility method: check start with
 */
ajaxComponent.util.startWith = function(value, needle) {
	var result = false;
	if (value) {
		result = (value.match("^" + needle) == needle);
	}
	return result;
};

/**
 * String utility method: check end with
 */
ajaxComponent.util.endWith = function(value, needle) {
	var result = false;
	if (value) {
		result = (value.match( needle + "$") == needle);
	}
	return result;
};
