/*
* Clean AJAX Engine v2.5
* Copyright 2005-2006 Carlos Eduardo Goncalves (cadu.goncalves@gmail.com)
*
* This program is free software, you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation.
*/

/**
 * Generic AJAX engine constructor. It provides global
 * engine features
 * @param debug
 *		Boolean flag that turns on/off the engine debug mode
 */
function Engine (debug){
  Engine.DEBUG = debug;
  if(Engine.DEBUG)
    Console.start();
}

/** HTTP supported methods */
Engine.HTTP_METHODS = ["GET", "POST"];

/** HTTP status codes */
Engine.HTTP_STATUS_CODES = [
	"200 OK", "201 Created", "202 Accepted", "203 Non-Authoritative Information",
	"205 Reset Content", "204 No Content", "206 Partial Content",
	"300 Multiple Choices", "301 Moved Permanently", "302 Found", "303 See Other",
	"304 Not Modified", "305 Use Proxy", "307 Temporary Redirect",
	"400 Bad Request", "401 Unauthorized", "402 Payment Required",
	"403 Forbidden",  "404 Not Found", "405 Method Not Allowed", "406 Not Acceptable",
	"407 Proxy Authentication Required", "408 Request Timeout", "409 Conflict",
	"410 Gone", "411 Length Required", "412 Precondition Failed",
	"413 Request Entity Too Large", " 414 Request-URI Too Long", "415 Unsupported Media Type",
	"416 Requested Range Not Satisfiable", "417 Expectation Failed",
	"500 Internal Server Error", "501 Not Implemented", "502 Bad Gateway",
	"503 Service Unavailable", "504 Gateway Timeout", "505 HTTP Version Not Supported"];

/**
* Choices the way how exceptions are showed
* @param message_id
*		Message unique identifier on the message queue
* @param
*		Exception to be displayed or threw
*/
Engine.reportException = function (msg_id, exception){
	if (Engine.DEBUG)
		Console.trace(exception, Console.EXCEPTION);
	else {
		msg = MessageQueue.messages[msg_id];
		if ((msg == null) || (msg.onError == null))
			throw exception;
		else
			msg.onError(exception);
		MessageQueue.messages[msg_id] = null;
	}
}

/**
* AJAX message constructor. It is based on value object design pattern
* and is used to avoid changes on method's signatures
*/
function Message(){
	this.id = null;
	this.method = null;
	this.address = null;
	this.xslt = null;
	this.value = null;
	this.document = document;
	this.consumer = null;
	this.onChange = null;
	this.onError = null;
}

/**
* AJAX connection constructor. It is provides all methods
* used to dispatch messages.
*/
function Connection(){}

/**
* Send a message after add it to message queue
* @param msg
*		Message value object
*/
Connection.sendMessage = function (msg){
	try {
		wrapper = MessageQueue.add(msg);
		msg.id = wrapper.id;
		wrapper.request.send(msg.value);
		Console.trace(msg, Console.REQUEST);
		return msg.id;
	} catch(e){
		Engine.reportException(msg.id, e);
	}
}

/**
* Copy a form to a message, and sent the message after add it to message queue
* @param msg
*		Message value object
* @param form
*		HTML form to be copy to the message
*/
Connection.sendFormByMessage = function (msg, form){
	try {
		msg.method = "POST";
		msg.value = ParserTool.formToUtf(form);
		wrapper = MessageQueue.add(msg);
		msg.id = wrapper.id;
		wrapper.request.setRequestHeader("Content-Type","application/x-www-form-urlencoded;charset=UTF-8");
		wrapper.request.send(msg.value);
		Console.trace(msg, Console.REQUEST);
		return msg.id;
	} catch(e) {
		Engine.reportException(msg.id, e);
	}
}

/**
* Abort a message request and remove it from the message queue
* @param msg
*		Message value object
* @param form
*		HTML form to be copy to the message
*/
Connection.abortMessage = function(msg_id){
	if (isNaN(msg_id))
		return;
	try {
		if(MessageQueue.messages[msg_id] != null){
		MessageQueue.messages[msg_id].request.abort();
		MessageQueue.messages[msg_id] = null;
	}
	} catch(e){Engine.reportException(msg_id, e);}
}


/**
* AJAX message queue constructor. It provides a simple
* queue to add messages in.
*/
function MessageQueue(){}

/** Array that hold all active messages */
MessageQueue.messages = new Array();

/**
* Add a new AJAX message on the queue
* @param msg
*		Message value object to be added
*/
MessageQueue.add = function (msg){
	try {
		wrapper = new MessageWrapper();
		wrapper.wrap(msg);
		MessageQueue.messages.push(wrapper);
		return wrapper;
	} catch(e) {
		Engine.reportException(MessageQueue.messages.length, e);
	}
}


/**
* AJAX Message wrapper constructor. It is based on wrapper design pattern and
* is used to adapt messages with objects provided by the browser
*/
function MessageWrapper(){}

/**
* Build client side JavaScript objects required to XMLHTTP request
*/
MessageWrapper.prototype.buildRequest = function (){
	try{
		obj = (typeof XMLHttpRequest != "undefined") ? new XMLHttpRequest() :
			this.buildActiveX(["Microsoft.XMLHTTP", "Msxml2.XMLHTTP"]);
		return obj;
	} catch(e) {
		Engine.reportException(null, e);
	}
}

/**
* Try to build an ActiveX object based on a list classes
* @param names
*		Array with ActiveX class names that could be created
*/
MessageWrapper.prototype.buildActiveX = function (names){
	var obj = null;
	for(var i = 0; i < names.length; i++) {
		try {
			obj = new ActiveXObject(names[i]);
			break;
		} catch (e) {}
	}
	return obj;
}

/**
* Load a file based on the provided url
* @param url
*		Universal resource locator of the file
*/
MessageWrapper.prototype.loadFile = function (url){
	try{
		req = this.buildRequest();
		req.open("GET", url, false);
		req.send('');
		return req.responseXML;
	} catch(e){
		Engine.reportException(null, e);
	}
}

/**
* Transforms an XML document based on an XSLT document
* @param origin
*		XML document to be transformed
* @param style
*		XSLT document used to perform the transformation
*/
MessageWrapper.prototype.transform = function (origin, style){
	try{
		if (typeof XSLTProcessor != "undefined"){
			proc = new XSLTProcessor();
			proc.importStylesheet(style);
			return (new XMLSerializer()).serializeToString(proc.transformToDocument(origin));
		}
		else{
			proc = this.buildActiveX(["Microsoft.XMLDOM", "MSXML2.DOMDocument", "MSXML.DOMDocument"]);
			proc.async = "false";
			proc.load(origin);
			return proc.transformNode(style);
		}
	} catch(e){
		Engine.reportException(null, e);
	}
}

/**
* Import all AJAX message data to the wrapper
* @param msg
*		Message value object to be wrapped
*/
MessageWrapper.prototype.wrap = function(msg){
	try{
		if(Engine.HTTP_METHODS.toString().indexOf(msg.method.toUpperCase()) == -1)
			msg.method = "POST";

		if(msg.xslt != null)
			this.style = this.loadFile(msg.xslt);

		this.id = MessageQueue.messages.length;
		this.consumer = msg.consumer;
		this.document = msg.document;
		this.address = msg.address;
		this.request = this.buildRequest();
		this.request.open(msg.method, msg.address);
		this.onError = msg.onError;

		if(msg.onChange != null)
			this.request.onreadystatechange = msg.onChange;
		else{
			var _this = this;
			var args = [this];
			this.request.onreadystatechange = function() {
				_this.onChange.apply(args[0], args);
			}
		}
	} catch(e){
		Engine.reportException(null, e);
	}
}

/**
* Internal onreadystatechange listener
*/
MessageWrapper.prototype.onChange = function() {
	if (this.request.readyState == 4) {
		if (this.request.status >= 200 && this.request.status <= 299) {
			s = this.parseStatus(this.request.status)  + ": " + this.address;
			Console.trace(s, Console.RESPONSE);
			locator = new DomIterator(this.document);

			if(this.style != null)
				locator.applyValue(this.consumer, this.transform(this.request.responseXML, this.style));
			else
				locator.applyValue(this.consumer, this.request.responseText);

			MessageQueue.messages[this.id] = null;
		}
		else{
			e = this.parseStatus(this.request.status) + ": " + this.address;
			Engine.reportException(this.id, e);
		}
	}
}

/**
* Return user friendly HTTP status code
* @param status
*		Status code
*/
MessageWrapper.prototype.parseStatus = function (status) {
	var str = "HTTP status " + status;
	for (var i = 0; i < Engine.HTTP_STATUS_CODES.length; ++i) {
		if (Engine.HTTP_STATUS_CODES[i].indexOf(status) != -1) {
			str = Engine.HTTP_STATUS_CODES[i];
			break;
		}
	}
	return str;
}

/**
* Document object model iterator constructor. It is based on iterator
* design pattern and is used to provide abstraction form DOM elements
* @param doc
*		Document used to iterate in
*/
function DomIterator(doc){
	this.doc = doc;
}

/**
* Search for an element on a document and change his value
* @param id
*		DOM element id attribute value
* @param value
*		Value to be applied on element
*/
DomIterator.prototype.applyValue = function (id, value) {
	try{
		el = this.doc.getElementById(id);
		iframes = this.doc.getElementsByTagName("iframe");
		for (var i = 0; i <= iframes.length; ++i) {
			if (iframes[i] != null) {
				if (iframes[i].id == el.id) {
				el.contentWindow.document.body.innerHTML = value;
				return;
				}
			}
		}

		if (el.value != null) {
			el.value = value;
			return;
		}

		if(el.innerHTML != null){
			el.innerHTML = value;
			return;
		}

	} catch(e){
		Engine.reportException(null, e);
	}
}


/**
* Multi-purpose parser constructor. It provides
* some parsing algoritms
*/
function ParserTool(){}

/**
* Encode the fields present on a form to UTF
* @param form
*		HTML form to encode
*/
ParserTool.formToUtf = function (form) {
	var utf = "";
	try{
		for (var i = 0; i < form.elements.length; ++i) {
			switch (form.elements[i].type.toLowerCase()) {
				case "button": break;
				case "reset": break;
				case "radio":
					if (form.elements[i].checked)
						utf += form.elements[i].name + "=" + escape(form.elements[i].value) + "&";
					break;
				case "checkbox":
					if (form.elements[i].checked)
						utf += form.elements[i].name + "=" + escape(form.elements[i].value) + "&";
					break;
				case "select-one":
					utf += form.elements[i].name + "=" + form.elements[i].options[form.elements[i].selectedIndex].value + "&";
					break;
				case "select-multiple":
					for (var v = 0; v < form.elements[i].options.length; ++v) {
						if (form.elements[i].options[v].selected)
							utf += form.elements[i].name + "=" + form.elements[i].options[v].value + "&";
					}
					break;
				default:
					utf += form.elements[i].name + "=" + escape(form.elements[i].value) + "&";
					break;
			}
		}

		utf = utf.substr(0,(utf.length - 1));
		return utf;
	} catch(e){
		Engine.reportException(null, e);
	}
}

/**
* Encode a JavaScript var to a HTML representation
* @param value
*		The var value
* @param id
*		HTML id attribute used to identify the object
*/
ParserTool.jsToHtml = function (value, id) {
	txt = (id) ? "<div class='hide' id='" + id + "'>" : "<div>";

	if (value instanceof Object)
		for (i in value) {
			txt += "<p><b>" + i + "</b> = " + value[i] + "<p>";
		}
	else
		txt += "<p>" + value + "</p>";

	return txt += "</div>";
}


/**
* Trace console constructor. It provides features to
* trace message data and to be aware about messages' life cycle.
*/
function Console(){}

/** Messages trace counter */
Console.counter = 1;
/** Window used to open the console */
Console.window = null;
/** Possible trace status */
Console.REQUEST = "request";
Console.RESPONSE = "response";
Console.EXCEPTION = "exception";

/**
* Start the console. It add a link to the current document
* that can be used to open the console window
*/
Console.start = function () {
	opener = document.createElement("div");
	opener.style.width = "100%";
	opener.align = "right";
	opener.style.height = "30px";
	opener.innerHTML="<a style='color:#000000;' href='javascript:Console.open()'>Clean AJAX Engine Console</a>";
	document.body.appendChild(opener);
}

/**
* Open the console window.
*/
Console.open = function() {
	Console.window = window.open("clean-console.html", "console", "height=350,width=450,scrollbars");
}

/**
* Check if the console window is open
*/
Console.isOpen = function() {
	if (Console.window == null)
		return false;
	else
		return (!(Console.window.closed) && (Console.window.document != null));
}

/**
* Add trace register on the message console
* @param value
*		Trace data to display
* @param status
*		The trace status (request || response || exception)
*/
Console.trace = function (value, status) {
	if (Console.isOpen()) {
		stack = ParserTool.jsToHtml(Console.counter, null);
		title = "<a href='javascript:expand(" + Console.counter + ")'>" + status.toUpperCase() + "<a>";
		data = ParserTool.jsToHtml(value, Console.counter);
		++Console.counter;
		table = Console.window.document.getElementById("trace_table");
		row = table.insertRow(-1);
		row.className = status;
		cell_1 = row.insertCell(-1);
		cell_2 = row.insertCell(-1);
		cell_1.vAlign = "top";
		cell_2.vAlign = "top";
		cell_1.innerHTML = stack;
		cell_2.innerHTML = title + data;
	}
}

// Engine start up. Set the boolean flag to turn on/off the debug mode
clean = new Engine(false);
