// dependencies: util.js

/*
 * To run this synchronous just pass
 * the word "sync" as onReadyState4-parameter
 */


/*
	description: This is a ajax-object. If you want to use the debug-functions, or use the built-in "history" you have to use 
	this object. For simple calls there are methods that abstracts this, see simplePost and the other simple_ functions.
*/
function Ajax() {
	this._def = {}; //holder for default-settings...
	this._requests = [];
	this._history = [];
	this._historyIndex = this.numberOfRequests = this.historyLength = 0;
}

/*
	description: This is a request-object. To make a request without the ajax-object or the simple_ functions
	you can use this object and then use it's suitable functions. This is sort of a middle-way. You can use
	the debug-functions but not the history.
*/
function Request(protocol, url, variables, onReadyState4, onReadyState3, headers, requestID, history, cache) {

	if(!protocol || !url) return false;

	var fileType = url.split(".").pop();

	url = (url.indexOf(':') == -1) ? 'http://' + window.location.host + '/' + url : url;
	variables = (protocol.toUpperCase() != 'GET') ? variables : null;

	if(onReadyState3 === true) onReadyState3 = onReadyState4;

	this.xmlHttp = this._createXmlHttpObject();
	this.properties = { "cache" : cache,  "onReadyState3" : onReadyState3, "onReadyState4" : onReadyState4, "cancelled" : false, "completed" : false, "fileType" : fileType, "history" : history, "protocol" : protocol , "requestID" : requestID , "responseTime" : 0, "url" : url, "variables" : variables || null , "when" : new Date(), "headers" : headers };
}

Ajax.prototype = {

	POST: function(url, variables, onReadyState4, headers, history, cache, requestID) { return this._dorequest('POST',url,variables,onReadyState4,headers,history,cache,requestID); },
	GET: function(url, onReadyState4, headers, history, cache, requestID) { return this._dorequest('GET',url,null,onReadyState4,headers,history,cache,requestID); },
	HEAD: function(url, onReadyState4, headers, history, cache, requestID) { return this._dorequest('HEAD',url,null,onReadyState4,headers,history,cache,requestID); },

	atHistoryIndex: function(historyIndex) { return this._requests[this._history[historyIndex]]; },
	defaultSettings: function(cache, history, url, onReadyState4, headers, variables, requestID) { this._def = { "cache" : cache, "history" : history, "url" : url, "onReadyState4" : onReadyState4, "variables" : variables || null, "requestID" : requestID, "headers" : headers}; },
	getRequest: function(requestID) { return (o = this._requests[requestID]) ? o : null; },

	stepHistory: function(dir) {
		if(dir == 0 && this._historyIndex > 0) this._historyIndex--;
		else if(dir == 1 && this._historyIndex < (this._history.length-1)) this._historyIndex++;
		else return false;

		return this._requests[this._history[this._historyIndex]].repeatHistory();
	},

	//internal methods. not tested for direct use

	_dorequest: function(protocol, url, variables, onReadyState4, headers, history, cache, i) {
		var d = this._def, recall = false;

		if(cache === undefined) cache = d.cache;
		if(history === undefined) history = d.history;
		if(i === undefined) i = d.requestID;
		if(!url) url = d.url;
		if(variables === undefined) variables = d.variables;
		if(!onReadyState4) onReadyState4 = d.onReadyState4;

		if(i || i === 0) {
			if(p = this._requests[i]) { //recall
				history = p.properties.history;
				recall = true;
			}
		} else i = this._requests.length;

		if(!(o = new Request(protocol, url, variables, onReadyState4, headers, i, history, cache))) return null;
		o.properties.parent = this;

		if(o.call() && !recall) {
			this.numberOfRequests++;
			this._requests[i] = o;
		}
		return o;
	},

	_createHistoryEntry: function(i) {
		var l = this._history.length;

		if(this._historyIndex != (l-1) && l > 0) {
			var tmp = this._history.splice(this._historyIndex+1, l);

			for(var u = 0, length = tmp.length; u < length; u++) { //remove unused entries
				this._requests[tmp[u]].properties.history = false;
				delete this._requests[tmp[u]].properties.historyIndex;
			}
			l =	this.historyLength = this._history.length;
		}

		this._history[l] = i;
		this._historyIndex = l;
		this.historyLength++;
		return l;
	}
}

Request.prototype = {

	cancel: function() { this.xmlHttp.abort(); },
	getResponseHeader: function(header) { return this.getAllResponseHeaders()[header.toLowerCase()]; },
	getAllResponseHeaders: function() { return this._parseheaders(); },

	call: function() {

		var p = this.properties, that = this, sync = (p.onReadyState4 == "sync") ? true : false;

		if(!sync && (p.onReadyState4 || p.onReadyState3)) this.xmlHttp.onreadystatechange = function() { //let's handle the response

			var state = that.xmlHttp.readyState;
			if((p.onReadyState4 && state == 4) || p.onReadyState3 && state == 3) that._handleresponse(state);
		};


		if(p.protocol == "GET")  { //to fix the cache-problem

			var time = (new Date()).getTime() + "" + Math.random()*314159265;

			if(p.url.indexOf("?") != -1) p.url += "&nocache=" + time;
			else p.url += "?nocache=" + time;

		} else {
			var variables = p.variables;

			if(variables && variables.length > 0) { //encode the data, but don't save it as encoded (to keep readability)

				var tmp;

				variables = variables.split("&");

				for(var i = 0, len = variables.length; i < len; i++) {
					tmp = variables[i].split("=");
					tmp[1] = encodeURIComponent(tmp[1]);
					variables[i] = tmp.join("=");
				}

				variables = variables.join("&");
			}
		}
		
		try {

			this.xmlHttp.open(p.protocol, p.url, (sync ? false : true ));
			if(p.headers) {
				for(var header in p.headers) {
					this.xmlHttp.setRequestHeader(header, p.headers[header]);
				}
			}
			this.start = (new Date()).getTime(); //needs a start-time
			this.xmlHttp.send(variables);

			if(sync) return this._evaluatecontenttype();

		} catch(e) { return false; }

		return true;
	},

	repeatHistory: function() {
		var p = this.properties;

		if(!p.history) return false;
		else if(!p.cache) this.call();
		else this._doresponse(p.response);

		return true;
	},

	//internal methods. not tested for direct use

 	_createXmlHttpObject: function() { //creates an xmlHttpObject
		try { return new XMLHttpRequest(); } catch(e) {}
		try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch(e) {}
		try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch(e) {}
		throw new Error("Failed to create a xmlHttpRequest-object which is a nescessity for Ajax. Ending...");
	},

	_handleresponse: function(state) { //the main response-handler

		var resp;

		var p = this.properties, xml = this.xmlHttp, st = xml.status;
		p.responseTime = (new Date()).getTime() - this.start;

		if(!st) {
			p.cancelled = true;
			return;
		}

		delete this.start; //a little housekeeping, will show up in debug otherwise

		if(st == 200) {

			p.completed = true;
			if(p.history && !p.historyIndex && p.parent) p.historyIndex = p.parent._createHistoryEntry(p.requestID);

			resp = this._evaluatecontenttype();
			
		} else {
			resp = st;
		}

		if(p.cache) p.response = resp;
		this._doresponse(state, resp);
	},

	_evaluatecontenttype: function() { //gets the header, checks the contenttype and returns the appropriate respons
		
		var p = this.properties, xml = this.xmlHttp;

		if(p.protocol == 'HEAD') return this._parseheaders();

		var responseHeader;

		//let's make filetype overriding
		if(p.fileType == "json") responseHeader = "text/json";
		else if(p.fileType == "xml") responseHeader = "text/xml";
		else responseHeader = xml.getResponseHeader("Content-Type");

		switch(responseHeader) {
			case 'text/json': case 'text/javascript': case 'applikation/javascript': case 'application/x-javascript':
				p.responseType = 'json';
				return fromJson(xml.responseText);
			case 'text/xml':
				p.responseType = 'xml';
				return decodeURIComponent(xml.responseXML);
			default:
				p.responseType = 'text';
				return decodeURIComponent(xml.responseText);
		}
	},

	_doresponse: function(state, response) { //makes the onReadyState4 or onReadyState3 and so on...

		var p = this.properties, c;

		if(state == 3) c = p.onReadyState3;
		else if(state == 4) c = p.onReadyState4;

		if(typeof c == 'function') c(response);
		else {
			if(typeof c == 'string') c = document.getElementById(c);
			c.innerHTML = response;
		}
	},

	_parseheaders: function() {
		var headers = this.xmlHttp.getAllResponseHeaders().split("\n"), o = {}, tmp;
		for(var i = 0, length = headers.length; i < length; i++) {
			if(headers[i]) {
				tmp = headers[i].split(":");
				o[tmp[0].trim(" ").toLowerCase()] = tmp[1].trim(" ");
			}
		}
		return o;
	}
}


//standalone functions. wrappers and stuff


function simplePost(url, variables, onReadyState4, onReadyState3) { return _request('POST', url, variables, onReadyState4, onReadyState3, { "Content-type" : "application/x-www-form-urlencoded" })}
function simpleGet(url, onReadyState4, onReadyState3) { return _request('GET', url, null, onReadyState4, onReadyState3); }
function simpleHead(url, onReadyState4) { return _request('HEAD', url, null, onReadyState4); }

function _request(protocol, url, variables, onReadyState4, onReadyState3, headers) { //internal to save some code...
	var request = new Request(protocol, url, variables, onReadyState4, onReadyState3, headers);
	return request.call();
}


//errorhandling


function ErrorHandler(url, saveOnce, saveAsJs) {

	window.ErrorHandling = true; //just to enable stuff to know that errors are handled. Not sure it is needed but doesn't hurt...

	if(saveOnce) saveAsJs = true;
	if(saveAsJs) this._errorlist = [];

	this._url = url || false;
	this._saveOnce = saveOnce;
	this._saveAsJs = saveAsJs;
	var that = this;

	//must use the old model since the new doesn't support onerror 100%
	window.onerror = function(message, url, line) {
		
		that._sendError(message, url, line);
		return true;
	};

	if(saveOnce) {
		addEvent(window, "unload", function() {
			that._saveOnUnload();
		});
	}
}

ErrorHandler.prototype = {

	//just returns the errorlist as an array
	getErrorList : function() {
		return this._errorlist;
	},
	
	//throws a new error manually
	throwError : function(message, functionName, line) {

		functionName = !functionName ? "" : "function: " + functionName;
		line = !line && line !== 0 ? -1 : line;

		this._sendError(message, functionName, line);
	},

	//internal methods. not tested for direct use

	_url : false,
	_errorlist : null,
	_saveOnce : false,
	_saveAsJs : false,
	_errorlist : null,

	
	//send the error via ajax to the server-side error-handler, as well as inserts it into the javascript-array (if told to)
	_sendError : function(message, url, line) {

		if(this._saveAsJs) this._addToList(message, url, line);

		if(this._url && !this._saveOnce) {

			try {
				simplePost(this._url, "message=" + message + "&line=" + line + "&url=" + url);
			} catch(e) {};
		}
	},

	//add to the errorlist
	_addToList : function(message, url, line) {

		//0 = message, 1 = url, 2 = line, 3 = number of times, 4 = first time in milliseconds, 5 = last time in milliseconds

		var inserted = false;

		for(var i = 0, len = this._errorlist.length; i < len; i++) {

			if(message == this._errorlist[i][0] && url == this._errorlist[i][1] && line == this._errorlist[i][2]) {
				this._errorlist[i][3]++;
				this._errorlist[i][5] = (new Date()).getTime();
				inserted = true;

				//fixa så nyaste alltid ligger först, mne bara att lagra nya felet i en variabel, ta bort den ur arrayen med aktuellt i och sen splica den till början
			}
		}

		if(!inserted) {
			var date = (new Date()).getTime();

			this._errorlist.splice(0, 0, [ message, url, line, 1, date, date ])

			//this._errorlist.push([ message, url, line, 1, date, date ]);
		}
	},

	//used to save the errorlist on unload, if specified
	_saveOnUnload : function() {

		if(this._errorlist.length > 0) {
			try {
				simplePost(this._url, "json=" + toJson(this._errorlist));
			} catch(e) {};
		}
	}
}


// history-functions


/* func should be a function that is run on each back/forward-navigation. it is provided
 * with the value stored in the url.
 * This function should be run on pageload
 */
function initHistory(func) { // provide a function that handles the value collected
	// kankse borde fixa så den kan tala om om det var back elelr forward, typ sätta in en räknare i hashen innan bokstaven och sen bara kolla den mot förra kanske eller nåt
	if(!func || typeof func != "function") return;
return;
	// check, create and append

	var internalFunction = function() {

		var info = null;

		if(window.isnew) {

			if(!window._newlyadded) info = window.location.hash.ltrim("#");
			else window._newlyadded = false;
		} else {

			var frame = id("____historyFrame");
			if(frame.noload) { // just to prevent onload to fire upon creation
				frame.noload = false;
				return;
			}

			if(!frame._newlyadded) {
				info = frameWindow(frame).location.search.ltrim("?");
				if(info) window.location.hash = "#" + info; // to enable bookmarking easily
				else if(info === "") window.location.hash = "";
			} else {
				frame._newlyadded = false;
			}
		}

		if(info !== null) func(_unhash(info)); // only call on history-navigation
	};

	var ieCheck = document.documentMode;

	if("onhashchange" in window && (!ieCheck || ieCheck > 7)) {

		window.isnew = true;
		window.onhashchange = internalFunction;
	} else {

		var historyFrame = id("____historyFrame");
		if(!historyFrame) {

			historyFrame = document.createElement("iframe");
			historyFrame.id = "____historyFrame";
			historyFrame.src = "tom.php";
			historyFrame.tabIndex = -1;
			historyFrame.title = "This is used for emulating history on browsers not supporting onhashchange and contains nothing.";
			historyFrame.noload = true; // don't run onload when created

			addCss(historyFrame, "display:none;height:0px;width:0px;");

			document.body.appendChild(historyFrame);
		}

		addEvent(historyFrame, "load", internalFunction);
	}
}

/* Use this to create a history-entry. Provide with
 * unhashed information to store in the url.
 */
function addToHistory(entry) {
return;

	if(!entry) return;

	window.location.hash = _hash(entry); // to enable bookmarking easily in older browsers, and ofc to set event in newer

	if(window.isnew) {
		window._newlyadded = true;
	} else {

		var historyFrame = id("____historyFrame");
		if(!historyFrame) return false;

		frameWindow(historyFrame).location.search = "?" + entry;

		historyFrame._newlyadded = true;
	}
}

// just some helper-functions

function _unhash(hashed) {

	hashed = hashed.ltrim("#");

	var unhashed = "";

	if(hashed.charAt(0) != "a") return hashed.ltrim("b"); // numeric
	else hashed = hashed.trim("a");

	var unhashed = "";

	var hashes = hashed.split(/0(?=[1-9])/g);
	
	for(var i = 0, len = hashes.length; i < len; i++)
		unhashed += String.fromCharCode(hashes[i]);

	return unhashed;
}

function _hash(unhashed) {

	var previousHash = window.location.hash;
	var counter = (!previousHash) ? 0 : previousHash.charAt(0);
	if(!counter) counter = 0;

	if(!unhashed) return false;
	if(!isNaN(unhashed)) return "b" + unhashed;

	var hashed = "a";

	for(var i = 0, len = unhashed.length; i < len; i++)
		hashed += "0" + unhashed.charCodeAt(i) + "";

	return "#" + hashed;
}
