AJAX = {
	/** Contains additional request data */
	_requests: [],

	/**
	 * Returns a generic AJAX object.
	 */
	_getObject: function() {
		if (window.XMLHttpRequest) {
			return new XMLHttpRequest();
		}
		else if (window.ActiveXObject) {
			return new ActiveXObject('Microsoft.XMLHTTP');
		}
		else {
			return false;
		}
	},

	/**
	 * Generic handler for onReadyStateChange.
	 */
	_onStateChange: function(obj) {
		// This can happen if the browser doesn't properly support cancel.
		// In that case we should ignore events.
		if(obj.cancelled)
			return;
		
		try {
			if(obj.xmlhttp.readyState == 4) {
				// We are ready. Check if the status was proper (200-299).
				if(obj.xmlhttp.status > 199 && obj.xmlhttp.status < 300) {
					// Fire success callback.
					obj.callback(obj.xmlhttp, obj.data);
				}
				else {
					// Fire error callback.
					obj.errorcallback(obj.xmlhttp, obj.data);
				}
			}
		}
		catch(e) {
			// This may happen in IE. It means that something isn't quite right.
			obj.errorcallback(obj.xmlhttp, obj.data);
		}
	},

	/**
	 * Performs a GET request. The callback will be called when the data is ready.
	 * You may pass additional data which will be passed to the callback.
	 * If no error callback is specified then the normal callback will be expected to handle errors.
	 * @param string URL to get
	 * @param function Callback to call: function(xmlhttp, data)
	 * @param mixed Data to pass to callback
	 * @param function Error callback to call: function(xmlhttp, data)
	 * @return FeltronAJAXWrapper
	 */
	GET: function(url, callback, data, errorcallback) {
		if(typeof errorcallback == 'undefined')
			errorcallback = callback;
			
		var obj = new AJAXWrapper(data, callback, errorcallback);
		AJAX._requests.push(obj);
		
		obj.xmlhttp.open('GET', url, true);
		obj.xmlhttp.onreadystatechange = function() {
			AJAX._onStateChange(obj);
		}
		obj.xmlhttp.send(null);
		
		return obj;
	},

	/**
	 * Performs a POST request. The callback will be called when the data is ready.
	 * You may pass additional data which will be passed to the callback.
	 * If no error callback is specified then the normal callback will be expected to handle errors.
	 * @param string URL to get
	 * @param string|array Data to POST; array with key => value or a prepared URLencoded string
	 * @param function Callback to call: function(xmlhttp, data)
	 * @param mixed Data to pass to callback
	 * @param function Error callback to call: function(xmlhttp, data)
	 * @return FeltronAJAXWrapper
	 */
	POST: function(url, postdata, callback, data, errorcallback) {
		if(typeof errorcallback == 'undefined')
			errorcallback = callback;
			
		var obj = new AJAXWrapper(data, callback, errorcallback);
		AJAX._requests.push(obj);
		
		if(typeof postdata == 'object') {
			var newdata = '', key;
			for(key in postdata) {
				if(newdata != '')
					newdata += '&';
				
				newdata += encodeURIComponent(key) + '=' + encodeURIComponent(postdata[key]);
			}
			postdata = newdata;
		}
		
		obj.xmlhttp.open('POST', url, true);
		obj.xmlhttp.onreadystatechange = function() {
			AJAX._onStateChange(obj);
		}

		obj.xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
		obj.xmlhttp.send(postdata);
		
		return obj;
	}
};

/**
 * The friendly AJAX wrapper object.
 */
function AJAXWrapper(data, callback, errorcallback) {
	this.xmlhttp = AJAX._getObject();
	this.data = data;
	this.callback = callback;
	this.errorcallback = errorcallback;
};

/**
 * Cancels a sent AJAX request. This will prevent any events from being sent.
 * @return void
 */
AJAXWrapper.prototype.cancel = function() {
	this.cancelled = true;
	
	// Abort if we can; IE6 does not support abort.
	if(typeof this.xmlhttp.abort == 'function') {
		this.xmlhttp.abort();
	}
}

/**
 * Manages user cookies.
 */
Cookie = {
	/**
	 * Sets a cookie.
	 * @param string Name of the cookie
	 * @param string Value of the cookie
	 * @param int (optional) Expiry date of the cookie; if zero or unspecified it will be a session cookie
	 * @return void
	 */
	set: function(name, value, expireMins) {
		if(typeof expireMins == 'undefined' || (expireMins == 0)) {
			document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value) + '; path=/';
		}
		else {
			var date = new Date();
			date.setTime(date.getTime() + (expireMins * 60 * 1000));
			document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value) + '; expires=' + date.toGMTString() + '; path=/';
		}
	},
	
	/**
	 * Retrieves the value of a named cookie.
	 * @param string Name of the cookie
	 * @return string Value or null if undefined
	 */
	get: function(name) {
		var cookies = document.cookie.split(';');
		for(var i = 0; i < cookies.length; ++i) {
			var cookiedata = cookies[i].split('=');
			if(decodeURIComponent(cookiedata[0].replace(' ', '')) == name) {
				return decodeURIComponent(cookiedata[1]);
			}
		}
		return null;
	}
}

// Favorites manager
Favorites = {
	/** The favorites array contains Game objects */
	favorites: [],
	
	/** Total amount of favorites that we have (including favorites which we don't know about) */
	totalFavorites: 0,
	
	/** Element to render in (set this from the HTML) */
	element: false,
	
	/** Load state */
	loaded: false,
	
	/** Have we encountered something we can't handle? */
	errored: false,
	
	/** ID of the current game (can be false); used to change the add & remove links */
	currentGameID: false,
	
	/** Favorite link element */
	favoriteLinkElement: false,
	
	/** Amount of elements to show (if no offset and limit are given to loadFavorites) */
	defaultLimit: 0,
	
	/** Optional, overrideable function that may set up pagination devices */
	paginatorSetup: function(totalGames) {},
	
	/** HTML for a single game */
	gameTemplateHTML: '',
	
	/** HTML for when we have no favorite games */
	noFavoritesHTML: '<p>You don\'t have any favorite games added yet. Click on "add to favorites" to get started!</p>',
	
	/** Text used for the add to favorites link */
	addToFavoritesText: 'Add to favorites',
	
	/** Text used for the remove from favorites link */
	removeFromFavoritesText: 'Remove from favorites',
	
	/**
	 * Asks the server for favorites data assuming that we have a session ID.
	 * @return bool Presence of cookie (if false, we are sure that we have no favorites)
	 */
	loadFavorites: function(offset, limit) {
		var games = Cookie.get('favorites');
		var limitstring = '';
		
		if(typeof offset != 'undefined') {
			limitstring += offset + '/';
			if(typeof limit != 'undefined')
				limitstring += limit + '/';
		}
		else if(Favorites.defaultLimit != 0) {
			limitstring = '0/' + Favorites.defaultLimit + '/';
		}
		
		if(games != null) {
			AJAX.GET('/favorites/get/' + limitstring, Favorites.onLoaded);
			return true;
		}
		else {
			// We can't possibly have any favorite games. Turn the link into the add link.
			Favorites.render();
			
			return false;
		}
	},
	
	/**
	 * This callback is called when we retrieve favorites.
	 * @param XMLHTTPRequest
	 */
	onLoaded: function(xmlHttp) {
		if(xmlHttp.responseText) {
			try {
				var data = eval('(' + xmlHttp.responseText + ')');
				
				// We shouldn't ever get an error back from the server seeing that we have a session ID at this point. Kind of handle it anyway.
				if(data.error) {
					Favorites.errored = true;
				}
				
				if(data.results) {
					Favorites.favorites = data.results;
					
					// Make the ShortDescription for all games
					for(var i = 0; i < Favorites.favorites.length; ++i) {
						var favorite = Favorites.favorites[i];
						
						// If there's a newline in this, only get everything before the newline
						if(favorite.Description.indexOf("\n") != -1) {
							favorite.Description = favorite.Description.slice(0, favorite.Description.indexOf("\n"));
						}
						
						// Cut the text short if needed
						if(favorite.Description.length < 60) {
							favorite.ShortDescription = favorite.Description;
						}
						else {
							favorite.ShortDescription = favorite.Description.substr(0, 55) + '...';
						}
						
						if (favorite.Name.length > 17)
							favorite.Name = favorite.Name.substr(0, 17) + '...';
					}
					
					Favorites.loaded = true;
					Favorites.render();
					
					// Check if we have a pagination function defined.
					if(Favorites.paginatorSetup) {
						Favorites.paginatorSetup(data.total);
					}
				}
			}
			catch(e) {
				Favorites.errored = true;
			}
		}
	},
	
	/**
	 * Adds a favorite by game ID.
	 * @param int Game ID
	 * @return void
	 */
	add: function(gameID) {
		AJAX.GET('/favorites/add/' + gameID, Favorites.onAdded, gameID);
	},
	
	onAdded: function(xmlHttp, gameID) {
		if(xmlHttp.responseText) {
			try {
				var data = eval('(' + xmlHttp.responseText + ')');
				
				// If we got an error, it means that it was already deleted in another tab (so reload).
				// If we got success, it means that it was deleted successfully (so reload).
				// In other words, we ignore the return value as long as we got something parsable.
				Favorites.loadFavorites();
				
				if(gameID == Favorites.currentGameID) {
					Favorites.setAddToFavoritesLink(true);
				}
			}
			catch(e) {
				Favorites.errored = true;
			}
		}
	},
	
	/**
	 * Removes a favorite by game ID.
	 * @param int Game ID
	 * @return void
	 */
	del: function(gameID, dontReload) {
		AJAX.GET('/favorites/del/' + gameID, Favorites.onDeleted, {"id": gameID, "dontreload": dontReload});
	},
	
	onDeleted: function(xmlHttp, stuff) {
		if(xmlHttp.responseText) {
			try {
				var data = eval('(' + xmlHttp.responseText + ')');
				
				// If we got an error, it means that it was already deleted in another tab (so reload).
				// If we got success, it means that it was deleted successfully (so reload).
				// In other words, we ignore the return value as long as we got something parsable.
				if(!stuff.dontreload) {
					Favorites.loadFavorites();
				}
				else {
					// Create a new array without the game we just deleted.
					var newgamedata = [];
					for(var i = 0; i < Favorites.favorites.length; ++i) {
						if(Favorites.favorites[i].GameID != stuff.id) {
							newgamedata.push(Favorites.favorites[i]);
						}
					}
					
					Favorites.favorites = newgamedata;
					Favorites.render();
				}
				
				if(stuff.id == Favorites.currentGameID) {
					Favorites.setAddToFavoritesLink(false);
				}
			}
			catch(e) {
				Favorites.errored = true;
			}
		}
	},
	
	/**
	 * Renders the favorite games list into the given element, emptying it beforehand.
	 * @return void
	 */
	render: function() {
		var element = Favorites.element;
		if(!element)
			return false;
		
		while(element.childNodes[0]) {
			element.removeChild(element.childNodes[0]);
		}
		
		if(Favorites.favorites.length) {
			element.innerHTML = '';
			for(var i = 0; i < Favorites.favorites.length; ++i) {
				var favdata = Favorites.favorites[i];
				var favhtml = Favorites.gameTemplateHTML;
				favhtml = favhtml.replace(/\$name/g, favdata.Name);
				favhtml = favhtml.replace(/\$id/g, favdata.GameID);
				favhtml = favhtml.replace(/\$url/g, favdata.URL)
				favhtml = favhtml.replace(/\$screenshot/g, favdata.Screenshot);
				favhtml = favhtml.replace(/\$description/g, favdata.Description);
				favhtml = favhtml.replace(/\$shortdescription/g, favdata.ShortDescription);
				element.innerHTML += favhtml;
			}
		}
		else {
			element.innerHTML = Favorites.noFavoritesHTML;
		}
	},
	
	/**
	 * Sets the state of the add to favorites link.
	 * @param bool Added state. If true, show the remove link. If false, show the remove link.
	 * @return void
	 */
	setAddToFavoritesLink: function(isAdded) {
		if(!Favorites.favoriteLinkElement)
			return;
		
		if(isAdded) {
			Favorites.favoriteLinkElement.innerHTML = Favorites.removeFromFavoritesText;
			Favorites.favoriteLinkElement.setAttribute('href', 'javascript:Favorites.del(' + Favorites.currentGameID + ')');
		}
		else {
			Favorites.favoriteLinkElement.innerHTML = Favorites.addToFavoritesText;
			Favorites.favoriteLinkElement.setAttribute('href', 'javascript:Favorites.add(' + Favorites.currentGameID + ')');
		}
	},
	
	/**
	 * Checks if have a given game ID favorited.
	 */
	checkGame: function(gameID, callback) {
		AJAX.GET('/favorites/check/' + gameID, function(xmlHttp) {
			if(xmlHttp.responseText != '') {
				try {
					var data = eval('(' + xmlHttp.responseText + ')');
					if(data.message == 'ALREADY_SAVED')
						callback(true);
					else
						callback(false);
				}
				catch(e) {
					Favorites.errored = true;
				}
			}
		});
	}
};
