//
//	search.js
//	All primary search related functions
//
//	dependencies:	/global/scripts/lib/prototype.js
//					/global/scripts/lib/effects.js
//					/global/scripts/lib/builder.js
//					/global/scripts/ac_quicktime.js
//					/global/scripts/search_triggers.js
//					/global/scripts/browserdetect.js
//					/global/scripts/lib/event_mixins.js
//
//

if (typeof(Search) == 'undefined') { Search = {}; }

Search.sanitize = function(text) {
	text = text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
	text = text.replace(/ /g, '+');
	text = text.replace(/[\"\'][\s]*javascript:(.*)[\"\']/g, "\"\"");
	text = text.replace(/&lt;script(.*)/g, "");
	text = text.replace(/eval\((.*)\)/g, "");
	return text;
}

Search.desanitize = function(text) {
	text = text.replace(/&lt;/g, "<").replace(/&gt;/g, ">");
	text = text.replace(/\+/g, ' ');
	return text;
}

/**
* Base class for search requests which use the proxy
*/
Search.ProxyRequest = function() {};
Search.ProxyRequest.prototype = {
	
	started: null,
	ended: null,
	
	s: null,
	r: null,

	baseUrl: null,
	url: null,
	
	activeRequest: null, //TODO only one request per search request?
	
	options: null,
	
	baseInitialize: function(server, request, options) {
		
		this.s = server;
		this.r = request;
		this.options = options;
		
		if (!this.options) {
			options = [];
		}
		
		if (typeof(this.options.onSuccess) != 'function') {
			this.options.onSuccess = Prototype.emptyFunction;
		}
		
		if (typeof(this.options.onFailure) != 'function') {
			this.options.onFailure = Prototype.emptyFunction;
		}
		
		var proxyPrefix = "/global/scripts/ajax_proxy.php?s=" + server;
		
		this.baseUrl = proxyPrefix + "&r=" + encodeURIComponent(request);
		
		//randomized param to prevent caching
		this.url = this.baseUrl
	},
	
	execute: function() {
		
		this.started = new Date();
		

		this.activeRequest = new Ajax.Request(this.url, {
			method: 'get', 
			onSuccess: this.options.onSuccess,
			onFailure: this.options.onFailure,
			onException: function(obj, err) {
				// throw(err)
				this.options.onFailure();
			}.bind(this)});
			
		return this.activeRequest;
	},
	
	abort: function() {
		if (this.activeRequest) {
			this.activeRequest.transport.abort();
		}
	},
	
	parseResponse: function(response) {},
	
	getDuration: function() {
		if (this.started && this.ended) {
			return this.ended .getTime() - this.started.getTime();
		} else {
			return 0;
		}
	}
	
};

/**
 * Base class for search requests which directly hit a json service
 */
Search.DirectRequest = function() {};
Search.DirectRequest.prototype = {
	
	started: null,
	ended: null,
	
	s: null,
	r: null,
	url: null,
	options: null,
	
	baseInitialize: function(server, request, options) {
		
		this.s = server;
		this.r = request;
		
		this.options = options;
		
		if (!this.options) {
			options = [];
		}
		
		if (typeof(this.options.onSuccess) != 'function') {
			this.options.onSuccess = Prototype.emptyFunction;
		}
		
		if (typeof(this.options.onFailure) != 'function') {
			this.options.onFailure = Prototype.emptyFunction;
		}
		
		//randomized param to prevent caching
		this.url = this.s + this.r + '&output=json&z=' + Math.floor(Math.random()*10000);
	},
	
	execute: function() {
		
		var callback = this.parseResponse.bind(this);
		Search.DirectCallbacks.push(callback);
		var callbackNumber = Search.DirectCallbacks.length - 1;
	
		var callbackString = encodeURIComponent('Search.DirectCallbacks[' + callbackNumber + ']');
		
		var scriptNode = document.createElement('script');
		scriptNode.setAttribute('charset', 'utf-8');
		scriptNode.setAttribute('type', 'text/javascript');
		scriptNode.setAttribute('src', this.url + '&callback=' + callbackString);
		
		var head = document.getElementsByTagName('head')[0];
		
		this.started = new Date();
		
		head.appendChild(scriptNode);
	},
	
	abort: Prototype.emptyFunction,
	
	parseResponse: function(response) {},
	
	getDuration: function() {
		if (this.started && this.ended) {
			return this.ended .getTime() - this.started.getTime();
		} else {
			return 0;
		}
	}
	
};

/**
 * Collection of callback functions exposed at this level so they can be
 * called by name via the script tag callback
 * 
 * Callbacks are pushed onto the collection when the request is "issued" by 
 * executing a Search.DirectRequest
 * 
 * //TODO have some way to remove a callback from this collection once it's handled
 */
Search.DirectCallbacks = [];

/**
* Search that hits the google boxes through the proxy.
*/
Search.FullSearchRequest = Class.create();
Object.extend(Search.FullSearchRequest.prototype, Search.ProxyRequest.prototype);
Object.extend(Search.FullSearchRequest.prototype, {
	
	s: 8,
	
	name: "Full Search",
	
	initialize: function(request, options) {
		this.baseInitialize(this.s, request, options);
		this.afterParsing = this.options.onSuccess;
		this.options.onSuccess = this.parseResponse.bind(this);
	},
	
	parseResponse: function(response) {
		
		this.ended = new Date();
		
		var responses = {};
		
		var rawResults = response.responseXML.getElementsByTagName('SearchResults')[0];
		
		if (!rawResults)  {
			// try {console.log("Invalid Response");} catch(e) {}
			throw("Invalid Response");
		}
		
		//Each category has its own response, or none, as a <GSP> element
		var categoryResponses = rawResults.getElementsByTagName('GSP');
		var params = null;
		for (var i = categoryResponses.length - 1; i >= 0; i--){
			
			categoryResponse = categoryResponses[i];
			
			//Parameters for this response are provided in <PARAM> elements
			params = categoryResponse.getElementsByTagName('PARAM');
			for (var j = params.length - 1; j >= 0; j--){
				
				if (params[j].getAttribute('name') == 'partialfields') {
					//Store the response under the correct key in out responses hash
					var categoryId = params[j].getAttribute('value').split(':')[1];
					responses[categoryId] = categoryResponse;
				} else if (params[j].getAttribute('value') == 'store_data'){
					responses['store'] = categoryResponse;
				}
				
				
			}
		}
		
		//TODO not duplicate this
		var getValue = function(container, tagName) {
			try{
				var ret = container.getElementsByTagName(tagName)[0].firstChild.nodeValue == '\n' ? container.getElementsByTagName(tagName)[0].childNodes[1].nodeValue : container.getElementsByTagName(tagName)[0].firstChild.nodeValue;
				return ret;
			}catch(e) {
				return '';
			}
		};
		
		//Find markup for jump shortcuts
		var jumpShortcuts = getValue(rawResults, 'Jump');
		
		//find spelling suggestions //TODO if necessary?
		var term = getValue(rawResults, 'Q');
		var suggestion = null;
		
		if (rawResults.getElementsByTagName('Spelling').length > 0) {
			suggestion = rawResults.getElementsByTagName('Spelling')[0].firstChild.getAttribute('q');
		}
		
		// try {console.debug("Parsed Responses: ", responses);} catch(e) {}
		
		this.afterParsing(this, responses, jumpShortcuts, suggestion); //TODO simply passing this along right now ot make sure this function is visited
		
	}
});

/**
 * Search that hits iTunes services directly
 * Tries to provide same client interface as any other search
 */
Search.iTunesSearchRequest = Class.create();
Object.extend(Search.iTunesSearchRequest.prototype, Search.DirectRequest.prototype);
Object.extend(Search.iTunesSearchRequest.prototype, {
	
	name: "iTunes Search",
	
	s: 'http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStoreServices.woa/wa/itmsSearch',
	afterParsing: Prototype.emptyFunction,
	
	initialize: function(request, options) {
		this.baseInitialize(this.s, request, options);
		this.afterParsing = this.options.onSuccess;
		this.options.onSuccess = this.parseResponse.bind(this);
	},
	
	parseResponse: function(response) {
		
		this.ended = new Date();
		
		var results = {itunes: response.results};
		this.afterParsing(this, results);
	}
});


Search.Category = function() {};
Object.extend(Search.Category.prototype, Event.Publisher);
Object.extend(Search.Category.prototype, {
	
	title: '',
	container: null,
	results: null,
	totalResultCount: 0,
	availableResultCount: 0,
	
	topResultCount: 5,
	maxResultCount: 10,
	maxDeepResultCount: 25,
	
	collapsing: false,
	showingOnlyTopResults: true,
	showingAllResults: false,
	
	resultList: null,
	
	//Meta information that reports on the state of the category
	meta: null,
	metaIndicator: null,
	metaNextAction: null,
	metaViewAll: null,
	
	//heading bar on category
	headingLocationIndicator: null, //indicates location within results available
	
	baseInitialize: function(title, container) {
		this.title = title;
		this.container = $(container);
		this.results = [];
	},
	
	createHeading: function(headingText) {
		
		this.headingLocationIndicator = Builder.node('span', {'class' : 'results'}, headingText);
		
		var heading = Builder.node('div', {'class' : 'heading'}, [
			Builder.node('h3', this.title),
			this.headingLocationIndicator
		]);
		
		return heading;
	},
	
	hasResults: function() {
		return this.availableResultCount > 0;
	},
	
	hide: function() {
		this.container.hide();
		Element.removeClassName(this.container, 'detailedresults');
	},
	
	show: function() {
		
		//Do not show categories with no results
		if(this.availableResultCount > 0) {
			this.container.show();
		}
		
	},
	
	expand: Prototype.emptyFunction,

	collapse: Prototype.emptyFunction
});

Search.TextCategory = Class.create();
Object.extend(Search.TextCategory.prototype, Search.Category.prototype);
Object.extend(Search.TextCategory.prototype, {
	
	startIndex: 0,
	endIndex: 9,
	
	resultsIndex: 0,
	options: null,
	
	isLoadingResults: false,
	
	initialize: function(title, container, options) {
		this.baseInitialize(title, container);
		
		this.options = options;
		
		if (!this.options) {
			this.options = [];
		}
		
	},
	
	populate: function(data) {
		
		// try {console.debug("Populating!", this, data);} catch(e) {}
		
		var getValue = function(container, tagName) {
			try{
				var ret = container.getElementsByTagName(tagName)[0].firstChild.nodeValue == '\n' ? container.getElementsByTagName(tagName)[0].childNodes[1].nodeValue : container.getElementsByTagName(tagName)[0].firstChild.nodeValue;
				return ret;
			}catch(e) {
				return '';
			}
		};
		
		// Estimated match count in <M>
		this.totalResultCount = parseInt(getValue(data, 'M'), 10);
		
		//correct estimated total matches found if we requested and expected N 
		//matches but only received N-M
		var resultsInfo = data.getElementsByTagName('RES')[0];
		if (resultsInfo) {

			var firstResultIndex = parseInt(resultsInfo.getAttribute('SN'), 10);
			var lastResultIndex = parseInt(resultsInfo.getAttribute('EN'), 10);
			
			// try {console.debug("Google Returned SH: " + firstResultIndex + " EN: " + lastResultIndex);} catch(e) {}
		
			var foundResultCount = lastResultIndex - firstResultIndex + 1;
			
			var requestedCount = this.showingAllResults ? this.maxDeepResultCount : this.maxResultCount;
			var expectedResultCount = Math.min(this.totalResultCount - firstResultIndex, requestedCount);
		
			// try {console.debug("Expected: ", expectedResultCount, "Found: ", foundResultCount);} catch(e) {}
		
			if (foundResultCount < expectedResultCount) {
				this.totalResultCount = this.availableResultCount + foundResultCount;
				// try {console.debug("!! We know for sure there are only " + this.totalResultCount + " results", this);} catch(e) {}
			}
		}
		
		if(isNaN(this.totalResultCount)) { 
			this.totalResultCount = 0;
			return;
		}
		
		//TODO what is this actually doing?
		var params = data.getElementsByTagName('PARAM');
		for (var i = params.length - 1; i >= 0; i--){
			
			var param = params[i];
			
			if (param.getAttribute('name') == 'num') {
				var value = parseInt(param.getAttribute('value'), 10); //TODO where should this be defined?
				if (value != 10) {
					this.iTopResults = value;
					this.iMaxResults = value;
					this.scope = true;
					this.num = value;
				}
			}
		}
		
		//TODO not sure what this value is all about...
		var start = 1;
		if (data.getAttribute('SN')) {
			var value = parseInt(data.getAttribute('SN'), 10);
			if (!isNaN(value)) {
				start = value;
			}
		}
		
		//Results for this response are provided in <R> elements
		var results = data.getElementsByTagName('R');
		for (var i = 0; i < results.length; i++) {
			
			var result = results[i];
			var title = null;
			var url = null;
			var description = null;
			
			//meta tag information for a result is found in <MT> elements
			var metaTags = result.getElementsByTagName('MT');
			for (var j = metaTags.length - 1; j >= 0; j--) {
				
				var metaTag = metaTags[j];
				
				if (metaTag.getAttribute('N') == 'Description') {
					if (metaTag.getAttribute('V') !== '') {
						description = metaTag.getAttribute('V');
					}
				}
			}
			
			title = getValue(result, 'T');
			url = getValue(result, 'U');
			description = description || getValue(result, 'S');
			
			this.push(title, url, description);
		}
		
		this.isLoadingResults = false;
		this.container.removeClassName('loading');
	},
	
	push: function(title, url, description) {
		//TODO does this need to be a class?
		result = {title: title, url: url, description: description};
		this.results.push(result);
		this.availableResultCount++;
	},
	
	render: function() {
		
		var headingLabel = this.getHeadingLocationLabel();
		var heading = this.createHeading(headingLabel);
		
		Event.observe(heading, 'click', function(evt) {
			Event.stop(evt);
			this.toggleCollapsed();
		}.bind(this));
		
		//build meta information display
		this.createMetaNode();
		
		//build results list node
		this.resultList = Builder.node('ul', {id: 'results-' + this.title + '-ul'});
		Element.addClassName(this.resultList, 'results');
		
		//TODO maybe limit this to the min(length of the list or max results count) ? Usually we only hit this on the first load so if we have Max + N results later on we stil probably have Max here at most
		this.renderResults(0, this.maxResultCount);
		
		this.container.appendChild(heading);
		this.container.appendChild(this.resultList);
		this.container.appendChild(this.meta);
	},
	
	renderResults: function(start, count) {
		var end = start + count;
		
		if (end > this.availableResultCount) {
			end = this.availableResultCount; //TODO if this is exclusive shoudln't it be the totalCount 0 <= N < Total ?
		}
		
		//readjust the indexed range to correctly reflect what's displayed
		this.startIndex = start;
		this.endIndex = end;
		
		// try {console.debug("Rendering results: " + this.startIndex + "-" + this.endIndex, this);} catch(e) {}
		
		this.resultList.innerHTML = '';
		
		for (var i = this.startIndex; i < this.endIndex; i++) {
			var result = this.results[i];
		
			var resultItem = this.createResultNode(result, i); //TODO eventually do we just want to hang onto the nodes themselves instead of the raw data?
			this.resultList.appendChild(resultItem);
		}
		
		this.meta.show();
		this.updateMetaInformation();
	},
	
	createResultNode: function(result, position) {
		
		// force conversion of xml encoded ampersand for Safari
		if(AC.Detector.isWebKit()) {
			for(var prop in result) {
				if (result[prop] && typeof(result[prop]) =='string') {
					result[prop] = result[prop].replace(/&#38;/g,'&');
				}
			}
		}

		var resultItem = document.createElement('li');
		
		if (position < this.topResultCount) {
			Element.addClassName(resultItem, 'top-results');
		}

		var resultLink = document.createElement('a');
		resultLink.setAttribute('href', result.url);
		resultLink.innerHTML = result.title;
		
		var resultHeadline = document.createElement('h4');
		resultHeadline.appendChild(resultLink);
		
		resultItem.appendChild(resultHeadline);

		var resultDescription = document.createElement('p');
		Element.addClassName(resultDescription, 'desc');
		resultDescription.innerHTML = result.description;
		resultItem.appendChild(resultDescription);

		return resultItem;
	},
	
	getMetaIndicatorLabel: function() {
		
		var indicatorLabel = "";
		
		if (this.showingOnlyTopResults) {

			if (this.availableResultCount > this.topResultCount) {
				indicatorLabel = 'Top ' + this.topResultCount + ' angezeigt |&nbsp;';
			} else {
				indicatorLabel = (this.availableResultCount==1) ? this.availableResultCount + ' Ergebnis angezeigt' : this.availableResultCount + ' Ergebnisse angezeigt';
			}
			
		} else {
			if (this.availableResultCount < this.maxResultCount) {
				indicatorLabel = 'Ergebnisse 1-'+this.availableResultCount+' Angezeigt |&nbsp;';
			} else {
				indicatorLabel = 'Ergebnisse 1-' + this.maxResultCount + ' Angezeigt |&nbsp;';
			}
		}
		
		return indicatorLabel;
	},
	
	getMetaNextActionLabel: function() {
		
		//TODO get rid of this conditional, seems like this and a few other behaviours change based on the state of the category, should just be swapping a strategy?
		
		var nextActionLabel = "";
		
		if (this.showingAllResults && this.startIndex - this.maxResultCount <= 0) {
			nextActionLabel = "Alle Kategorien";
		} else if (this.showingAllResults) {
			nextActionLabel = "Frühere Ergebnisse";
		} else if (this.showingOnlyTopResults) {
			var nextCount = (Math.min(this.availableResultCount, this.maxResultCount) - this.topResultCount);
			nextActionLabel = (nextCount == 1) ? 'Nächstes ' + nextCount + ' Ergebnis' : 'Nächste ' + nextCount + ' Ergebnisse';
		} else  {
			var topCount = Math.min(this.availableResultCount, this.topResultCount);
			nextActionLabel = 'Top ' + topCount + ' Ergebnisse';
		}
		
		return nextActionLabel;
	},
	
	getHeadingLocationLabel: function() {
		var label = Math.min(this.availableResultCount, this.maxResultCount) + ' Ergebnisse von ca ' + this.totalResultCount;
		
		if (this.availableResultCount == 1) {
			label = '1 Ergebnis von ca ' + this.totalResultCount;
		}
		
		if (this.showingAllResults) {
			
			
			var end = (this.endIndex + 1) >= this.totalResultCount ? this.totalResultCount : this.endIndex;
			label = (this.startIndex + 1) + '-' + end + ' von ca ' + this.totalResultCount;
		}
		
		return label;
	},
	
	createMetaNode: function() {
		
		this.meta = document.createElement('p');
		Element.addClassName(this.meta, 'meta');
		
		this.metaViewAll = document.createElement('a');
		this.metaViewAll.setAttribute('href', '#'); //TODO better/useful link
		Element.addClassName(this.metaViewAll, 'viewall');
		this.metaViewAll.innerHTML = 'Weitere Ergebnisse';
		
		if (this.totalResultCount > this.maxResultCount || this.scope) {
			this.meta.appendChild(this.metaViewAll);
			
			Event.observe(this.metaViewAll, 'click', function(evt) {
				Event.stop(evt);
				if (!this.showingAllResults) {
					this.showMoreResults();
				} else {
					this.showNextResults();
				}
			}.bind(this));
		}
		
		this.metaIndicator = document.createElement('span');
		this.metaIndicator.innerHTML = this.getMetaIndicatorLabel();
		this.meta.appendChild(this.metaIndicator);
		
		this.metaNextAction = document.createElement('a');
		this.metaNextAction.setAttribute('href', '#'); //TODO better/useful link
		this.metaNextAction.innerHTML = this.getMetaNextActionLabel();
			
		Event.observe(this.metaNextAction, 'click', function(evt) {
			Event.stop(evt);
			if (!this.showingAllResults) {
				this.toggleAvailableResults();
			} else if (0 === this.startIndex) {
				this.showLessResults();
			} else {
				this.showPreviousResults();
			}
		}.bind(this));
			
		if (this.availableResultCount > this.topResultCount) {
			this.meta.appendChild(this.metaNextAction);
		}
	},

	toggleAvailableResults: function() {
		
		if(this.showingOnlyTopResults) {
			this.showAvailableResults();
		} else {
			this.showOnlyTopResults();
		}
	},

	showOnlyTopResults: function() {
		
		if (this.availableResultCount <= 0) {
			return;
		}
		
		this.container.removeClassName("show-all");
		this.meta.removeClassName('expandedmeta');
		this.showingOnlyTopResults = true;
		
		//ensure we are only looking at the first "page" of results where the
		//top results would be
		this.showLessResults();
		
		this.updateMetaInformation();
	},
	
	showAvailableResults: function() {
		
		if (this.availableResultCount <= 0) {
			return;
		}
		
		this.container.addClassName("show-all");
		this.meta.addClassName('expandedmeta');
		this.showingOnlyTopResults = false;
		this.updateMetaInformation();
	},
	
	showMoreResults: function() {
		
		if (this.showingAllResults || this.availableResultCount <= 0) {
			return;
		}
		
		Element.addClassName(this.container, 'detailedresults');
		this.showingAllResults = true;
		this.dispatchEvent('showmore', this);
		
		this.updateMetaInformation();
		Element.addClassName(this.metaNextAction, 'previous');
		
		// try {console.debug("Opening detailed search results", this);} catch(e) {}
		
		this.showNextResults();
		
	},
	
	showLessResults: function() {
		
		if (!this.showingAllResults || this.availableResultCount <= 0) {
			return;
		}
		
		this.showingAllResults = false;
		Element.removeClassName(this.container, 'detailedresults');
		
		this.dispatchEvent('showless', this);
		
		Element.removeClassName(this.metaNextAction, 'previous');
		
		// try {console.debug("Showing less detailed results", this);} catch(e) {}
		
		
		//TODO this overlaps most of the functionality of showPreviousResults, I'm not sure what I think of that at this point
		this.startIndex = 0;
		this.endIndex = this.startIndex + Math.min(this.maxResultCount, this.totalResultCount);
		
		this.renderResults(this.startIndex, this.endIndex);
		this.updateMetaInformation();
	},
	
	showNextResults: function() {
		
		if (this.endIndex == this.totalResultCount) {
			// try {console.debug("Not displaying");} catch(e) {}
			return;
		}
		
		var proposedStart = this.endIndex;
		var proposedEnd = ((this.endIndex + this.maxDeepResultCount) <= this.totalResultCount) ? this.endIndex + this.maxDeepResultCount : this.totalResultCount;
		
		// try {console.debug("show next results", this, proposedStart + "-" + proposedEnd + "/" + this.totalResultCount);} catch(e) {}
		
		if (this.availableResultCount >= proposedEnd) {
			this.renderResults(proposedStart, this.maxDeepResultCount);
		} else {
			this.getResults(proposedStart, this.maxDeepResultCount);
		}
		
		var effect = new Effect.ScrollTo(this.container, {
			duration: 0.3});
	},
	
	showPreviousResults: function() {
		
		if (this.startIndex === 0) {
			return;
		}
		
		var proposedStart = this.startIndex - this.maxDeepResultCount; //starting index inclusive
		
		if (proposedStart < 0) {
			proposedStart = 0;
		}
		
		var proposedEnd = proposedStart + this.maxDeepResultCount + 1; //ending index exclusive
		
		if (proposedEnd > this.totalResultCount) {
			proposedEnd = this.totalResultCount;
		}
		
		// try {console.debug("Show previous results", this, proposedStart + "-" + proposedEnd + "/" + this.totalResultCount);} catch(e) {}
		
		if (proposedStart <= 0) {
			this.showLessResults();
		} else if (this.availableResultCount >= proposedEnd) {
			this.renderResults(proposedStart, this.maxDeepResultCount);
		} else {
			this.getResults(proposedStart, this.maxDeepResultCount);
		}
		
		var effect = new Effect.ScrollTo(this.container, {
			duration: 0.3});
	},
	
	getResults: function(startIndex, resultCount) {
		
		//TODO getResults shouldn't know exaclty what do with the results outside of populating them and then passing control along to some handler
		
		if (this.isLoadingResults) {
			// try {console.debug("Not issuing another request, still waiting on one.");} catch(e) {}
			return;
		}
		
		this.isLoadingResults = true;
		this.container.addClassName('loading');
		this.resultList.innerHTML = "";
		this.meta.hide();
		

		var requestUrl = this.options.requestUrl + '&filter=1' + '&start=' + startIndex + '&num=' + resultCount + '&scope=' + this.options.scope;
		// try {console.debug("Issue request for " + resultCount + " results starting at index " + startIndex + "(Actually " + (startIndex - 1) + ")", requestUrl);} catch(e) {}
		
		var request = new Search.FullSearchRequest(requestUrl, {
			onSuccess: function(request, responses) {
				//populate using the correct response, though there' probably only going to be one
				this.populate(responses[this.options.scope]);
				this.renderResults(startIndex, resultCount);
			}.bind(this),
			
			onException: function() {
				this.isLoadingResults = false;
				this.container.removeClassName('loading');
				this.showOnlyTopResults();
			}.bind(this)});
		
		request.execute();
		
		
		//TODO all of this, but probably throughout the rest of this class
		//verify the start and end numbers as per scott's email
			//change the total results number if necessary
			//update the meta information if nexcessary
			
		//render new results or render "error"
	},
	
	updateMetaInformation: function() {
		
		this.metaIndicator.innerHTML = this.getMetaIndicatorLabel();
		this.metaNextAction.innerHTML = this.getMetaNextActionLabel();
		this.headingLocationIndicator.innerHTML = this.getHeadingLocationLabel();
		
		if(this.showingAllResults && this.endIndex == this.totalResultCount) {
			this.metaViewAll.hide();
		} else {
			this.metaViewAll.show();
		}
	},
	
	toggleCollapsed: function() {
		if (this.container.hasClassName('collapsed')) {
			this.expand();
		} else {
			this.collapse();
		}
	},
	
	collapse: function(){
		
		if (this.collapsing) {
			return;
		}
		
		this.collapsing = true;
		
		this.container.addClassName('collapsed');
		
		this.meta.hide();
		
		var effect = new Effect.BlindUp(this.resultList, {
			duration: 0.3,
			queue: {position:'end', scope:'toggleTopResults'}, //TODO unique queue per category
			afterFinish: function() {
				this.collapsing = false;
			}.bind(this)
		});
	},
	
	expand: function() {
		
		if (this.collapsing) {
			return;
		}
		
		this.collapsing = true;
		
		var effect = new Effect.BlindDown(this.resultList, {
			duration: 0.3,
			queue: {position:'end', scope:'toggleTopResults'}, //TODO unique queue per category
			afterFinish: function() {
				this.container.removeClassName('collapsed');
				this.meta.show();
				this.collapsing = false;
			}.bind(this)
		});
	},
	
	show: function() {
		//shadows the base show functionality to ensure text categories
		//are not deep when goign back to all categories shallow view
		
		//Do not show categories with no results
		if(this.availableResultCount > 0) {
			// try {console.debug("Requested to show! ", this);} catch(e) {}
			this.container.show();
		}
		
	}
	
});

Search.StoreCategory = Class.create();
Object.extend(Search.StoreCategory.prototype, Search.TextCategory.prototype);
Object.extend(Search.StoreCategory.prototype, {
	
	term: '',
	
	populate: function(data) {
		
		// try {console.debug("Populating Store!", this, data);} catch(e) {}
		
		var getValue = function(container, tagName) {
			try{
				var ret = container.getElementsByTagName(tagName)[0].firstChild.nodeValue == '\n' ? container.getElementsByTagName(tagName)[0].childNodes[1].nodeValue : container.getElementsByTagName(tagName)[0].firstChild.nodeValue;
				return ret;
			}catch(e) {
				return '';
			}
		};
		
		// Estimated match count in <M>
		this.totalResultCount = parseInt(getValue(data, 'M'), 10);
		
		//correct estimated total matches found if we requested and expected N 
		//matches but only received N-M
		var resultsInfo = data.getElementsByTagName('RES')[0];
		if (resultsInfo) {

			var firstResultIndex = parseInt(resultsInfo.getAttribute('SN'), 10);
			var lastResultIndex = parseInt(resultsInfo.getAttribute('EN'), 10);
			
			// try {console.debug("Google Returned SH: " + firstResultIndex + " EN: " + lastResultIndex);} catch(e) {}
		
			var foundResultCount = lastResultIndex - firstResultIndex + 1;
			
			var requestedCount = this.showingAllResults ? this.maxDeepResultCount : this.maxResultCount;
			var expectedResultCount = Math.min(this.totalResultCount - firstResultIndex, requestedCount);
		
			// try {console.debug("Expected: ", expectedResultCount, "Found: ", foundResultCount);} catch(e) {}
		
			if (foundResultCount < expectedResultCount) {
				this.totalResultCount = this.availableResultCount + foundResultCount;
				// try {console.debug("!! We know for sure there are only " + this.totalResultCount + " results", this);} catch(e) {}
			}
		}
		
		if(isNaN(this.totalResultCount)) { 
			this.totalResultCount = 0;
			return;
		}
		
		//TODO what is this actually doing?
		var params = data.getElementsByTagName('PARAM');
		for (var i = params.length - 1; i >= 0; i--){
			
			var param = params[i];
			
			if (param.getAttribute('name') == 'num') {
				var value = parseInt(param.getAttribute('value'), 10); //TODO where should this be defined?
				if (value != 10) {
					this.iTopResults = value;
					this.iMaxResults = value;
					this.scope = true;
					this.num = value;
				}
			}
		}
		
		//TODO not sure what this value is all about...
		var start = 1;
		if (data.getAttribute('SN')) {
			var value = parseInt(data.getAttribute('SN'), 10);
			if (!isNaN(value)) {
				start = value;
			}
		}
		
		//Results for this response are provided in <R> elements
		//TODO extract how we identify a single result? seem like a lot of duplication prior to this point between this and the base textcategory class
		var results = data.getElementsByTagName('R');
		for (var i = results.length - 1; i >= 0; i--) {
			
			var result = results[i];
			var title = null;
			var url = null;
			var description = null;
			var imageUrl = null;
			var rank = 0;
			
			//meta tag information for a result is found in <MT> elements
			var metaTags = result.getElementsByTagName('MT');
			for (var j = metaTags.length - 1; j >= 0; j--) {
				
				var metaTag = metaTags[j];
				
				//TODO reduce copy/paste
				if (metaTag.getAttribute('N') == 'searchDescription') {
					if (metaTag.getAttribute('V') !== '') {
						description = metaTag.getAttribute('V');
					}
				} else if(metaTag.getAttribute('N') == 'displayName') {
					if (metaTag.getAttribute('V') !== '') {
						title = metaTag.getAttribute('V');
					}
				} else if(metaTag.getAttribute('N') == 'searchImage') {
					if (metaTag.getAttribute('V') !== '' ) {
						imageUrl = metaTag.getAttribute('V');
					}
				} else if(metaTag.getAttribute('N') == 'salesRank') {
					if (metaTag.getAttribute('V') !== '' ) {
						rank = parseInt(metaTag.getAttribute('V'), 10);
					}
				}
			}
			
			url = getValue(result, 'U');
			
			//we're requiring a description to be a vlid result 
			if (description) {
				this.push(title, url, description, imageUrl, rank);
			}
		}
		
		this.sort();
		
		this.isLoadingResults = false;
		this.container.removeClassName('loading');
	},
	
	push: function(title, url, description, imageUrl, rank) {
		//TODO does this need to be a class?
		result = {
			title: title, 
			url: url, 
			description: description, 
			imageUrl: imageUrl, 
			rank: rank};
			
		this.results.push(result);
		this.availableResultCount++;
	},
	
	sort: function() {
		
		var sortByRank = function(resultA, resultB) {
			
			var a = resultA.rank;
			var b = resultB.rank;
			
			//higher ranks come first
			if (a > b)  {
				return -1;
			} else if (a < b) {
				return 1;
			} else {
				return 0;
			}
		}
		
		this.results.sort(sortByRank);
		// try {console.debug(this.results);} catch(e) {}
	},
	
	createResultNode: function(result, position) {
		
		// force conversion of xml encoded ampersand for Safari
		if(AC.Detector.isWebKit()) {
			for(var prop in result) {
				if (result[prop] && typeof(result[prop]) =='string') {
					result[prop] = result[prop].replace(/&#38;/g,'&');
				}
			}
		}

		var resultItem = document.createElement('li');
		
		if (position < this.topResultCount) {
			Element.addClassName(resultItem, 'top-results');
		}

		var resultImage = document.createElement('img');
		resultImage.setAttribute('src', result.imageUrl);
		Element.addClassName(resultImage, 'thumb');
		resultItem.appendChild(resultImage);

		var resultLink = document.createElement('a');
		resultLink.setAttribute('href', result.url);
		resultLink.innerHTML = result.title;
		
		var resultHeadline = document.createElement('h4');
		resultHeadline.appendChild(resultLink);
		
		resultItem.appendChild(resultHeadline);

		var resultDescription = document.createElement('p');
		Element.addClassName(resultDescription, 'desc');
		resultDescription.innerHTML = result.description;
		resultItem.appendChild(resultDescription);

		return resultItem;
	},
	
	createMetaNode: function() {
		
		this.meta = document.createElement('p');
		Element.addClassName(this.meta, 'meta');
		
		this.metaViewAll = document.createElement('a');
		this.metaViewAll.setAttribute('href', 'http://store.apple.com/Apple/WebObjects/germanstore?jknr=' + encodeURIComponent(Search.desanitize(this.term)));
		Element.addClassName(this.metaViewAll, 'viewall');
		Element.addClassName(this.metaViewAll, 'alwaysshow');
		this.metaViewAll.innerHTML = 'Alle Apple Store Ergebnisse';
		this.meta.appendChild(this.metaViewAll);
		
		this.metaIndicator = document.createElement('span');
		this.metaIndicator.innerHTML = this.getMetaIndicatorLabel();
		this.meta.appendChild(this.metaIndicator);
		
		this.metaNextAction = document.createElement('a');
		this.metaNextAction.setAttribute('href', '#'); //TODO better/useful link
		this.metaNextAction.innerHTML = this.getMetaNextActionLabel();
			
		Event.observe(this.metaNextAction, 'click', function(evt) {
			Event.stop(evt);
			if (!this.showingAllResults) {
				this.toggleAvailableResults();
			} else if (0 === this.startIndex) {
				this.showLessResults();
			} else {
				this.showPreviousResults();
			}
		}.bind(this));
			
		if (this.availableResultCount > this.topResultCount) {
			this.meta.appendChild(this.metaNextAction);
		}
	}
	
});

Search.iTunesCategory = Class.create();
Object.extend(Search.iTunesCategory.prototype, Search.Category.prototype);
Object.extend(Search.iTunesCategory.prototype, {
	
	term: '',
	
	subCategories: null,
	currentSubCategory: null,
	subCategoryList: null,
	
	contentArea: null,
	
	previewMovie: null,
	previewController: null,
	
	initialize: function(title, container) {
		this.baseInitialize(title, container);
		
		this.subCategoryList = $(container).getElementsByClassName('subcategories')[0];
		
		//subcateogires will need this when they're initialized
		this.contentArea = document.createElement('div');
		Element.addClassName(this.contentArea, 'sliderArea');
		
		//TODO this seems like a lot of code for this...it's only a sketch atm though I guess
		
		this.songCategory = new Search.iTunesSubCategory("Songs", this.contentArea);
		this.musicVideoCategory = new Search.iTunesSubCategory("Music Videos", this.contentArea);
		this.movieCategory = new Search.iTunesSubCategory("Movies", this.contentArea, 'movies');
		this.tvCategory = new Search.iTunesSubCategory("TV Shows", this.contentArea, 'tvshows');
		this.podcastCategory = new Search.iTunesSubCategory("Podcasts", this.contentArea);
		this.audiobookCategory = new Search.iTunesSubCategory("Audiobooks", this.contentArea);

		//Ordered subcategory listing
		this.subCategories = [
			this.songCategory,
			this.musicVideoCategory,
			this.tvCategory,
			this.movieCategory,
			this.podcastCategory,
			this.audiobookCategory];
		
		//mapping of mediaType key for trackWrapper'd items in iTS results
		//audio books are wrapped as a playlist so we just make a special 
		//excpetion for those when populating the sub categories
		this.mediaTypeMap = {
			song: this.songCategory,
			'feature-movie': this.movieCategory,
			podcast: this.podcastCategory,
			'music-video': this.musicVideoCategory,
			'tv-episode': this.tvCategory
		};
	},
	
	populate: function(results) {
		
		for (var i = 0; i < results.length; i++) {
			this.push(results[i]);
		}
		
		//TODO this.results isn't getting populated..as each sub category has its own results[]
	},
	
	push: function(result) {
		
		if (result.wrapperType == "track" && this.mediaTypeMap[result.mediaType]) {
			
			this.mediaTypeMap[result.mediaType].push(result);
			
		} else if (result.wrapperType == "playlist") {
			//TODO this would be where audiobooks get handled..among other things?
		} else {
			return;
		}
		
		this.availableResultCount++;
		this.totalResultCount++;
	},
	
	render: function() {
		
		//area for sub category headings to be listed
		var headingList = this.container.getElementsByClassName('subcategories')[0];
		
		var headingText = (this.availableResultCount == 1) ? this.availableResultCount + ' Ergebnis' : this.availableResultCount + ' Ergebnisse';
		
		var heading = this.createHeading(headingText);
		
		Event.observe(heading, 'click', function(evt) {
			Event.stop(evt);
			this.toggleCollapsed();
		}.bind(this));
		
		
		var compareSubCategories = function(a, b) {
			
			//higher populated categories come first in list
			
			var aResults = a.availableResultCount;
			var bResults = b.availableResultCount;
			
			if (aResults > bResults) {
				return -1;
			} else if (aResults < bResults) {
				return 1;
			} else {
				return 0;
			}
		};
		
		//we want the sub category with the most results shown first
		//TODO profile this
		this.subCategories.sort(compareSubCategories);
		
		this.container.appendChild(heading);
		this.container.appendChild(headingList);
		this.container.appendChild(this.contentArea);
		
		for (var i = 0; i < this.subCategories.length; i++) {
			var subCategory = this.subCategories[i];
			if (subCategory.hasResults()) {
				subCategory.render();
				
				if (i > 0) {
					subCategory.close();
				} else {
					this.openSubCategory(subCategory);
				}
				
				Event.observe(subCategory.heading, 'mousedown', this.openSubCategory.bind(this, subCategory));
				
			}
		}
		
		this.meta = document.createElement('p');
		Element.addClassName(this.meta, 'meta');
		
		var storeLink = document.createElement('a');
		Element.addClassName(storeLink, 'viewall');
		Element.addClassName(storeLink, 'alwaysshow');
		storeLink.innerHTML = "Alle iTunes Ergebnisse";
		
		var protocol = (iTunesCheck()) ? 'itms' : 'http';
		var storeUrl = protocol + '://ax.phobos.apple.com.edgesuite.net/WebObjects/MZSearch.woa/wa/com.apple.jingle.search.DirectAction/search?term=' + encodeURIComponent(Search.desanitize(this.term));
		
		storeLink.setAttribute('href', storeUrl);
		this.meta.appendChild(storeLink);
		
		this.container.appendChild(this.meta);

	},
	
	openSubCategory: function(subcategory) {
		
		if (this.currentSubCategory == subcategory) {
			return;
		}
		
		if (this.currentSubCategory) {
			this.currentSubCategory.close();
		}
		
		subcategory.open();
		
		this.currentSubCategory = subcategory;
	},
	
	toggleCollapsed: function() {
		if (this.container.hasClassName('collapsed')) {
			this.expand();
		} else {
			this.collapse();
		}
	},
	
	collapse: function() {
		
		if (this.collapsing) {
			return;
		}
		
		this.collapsing = true;
		
		this.container.addClassName('collapsed');
		
		this.meta.hide();
		
		var effect = new Effect.BlindUp(this.contentArea, {
			duration: 0.3,
			queue: {position:'end', scope:'toggleTopResults'}, //TODO unique queue per category
			afterFinish: function() {
				this.subCategoryList.hide();
				this.collapsing = false;
			}.bind(this)
		});
	},
	
	expand: function() {
		
		if (this.collapsing) {
			return;
		}
		
		this.collapsing = true;
		this.subCategoryList.show();
		
		var effect = new Effect.BlindDown(this.contentArea, {
			duration: 0.3,
			queue: {position:'end', scope:'toggleTopResults'}, //TODO unique queue per category
			afterFinish: function() {
				this.collapsing = false;
				this.container.removeClassName('collapsed');
				this.meta.show();
			}.bind(this)
		});
		
	}
});

Search.iTunesSubCategory = Class.create();
Object.extend(Search.iTunesSubCategory.prototype, Search.Category.prototype);
Object.extend(Search.iTunesSubCategory.prototype, {
	
	maxResultCount: 16,
	
	heading: null,
	resultList: null,
	resultsClass: null,
	
	initialize: function(title, container, resultsClass) {
		this.baseInitialize(title, container);
		this.resultsClass = resultsClass;
	},
	
	push: function(result) {
		
		if (this.availableResultCount == this.maxResultCount) {
			return;
		}
		
		this.results.push(result);
		this.availableResultCount++;
		this.totalResultCount++;
	},
	
	render: function() {
		
		var id = 'subcategory-' + this.title.replace(' ', '').toLowerCase();
		this.heading = $(id);
		Element.addClassName(this.heading, 'available');
		
		this.resultList = document.createElement('ul');
		Element.addClassName(this.resultList, 'albumresults');
		
		var items = [];
		
		for (var i = 0; i < this.results.length; i++) {
			var resultItem = this.createResultNode(this.results[i], i);
			this.resultList.appendChild(resultItem);
			items.push(resultItem);
		}
		
		
		if (items.length > 4) {
			var list = this.resultList;
			this.resultList = document.createElement('div');
			Element.addClassName(this.resultList, 'ACSliderMaskDiv');
			this.resultList.appendChild(list);
			
			//TODO extract duplicate code
			if (this.resultsClass) {
				Element.addClassName(this.resultList, this.resultsClass);
			}
			
			this.container.appendChild(this.resultList);
			//result list needs to be in dom so we can find dimensions
			this.slider = new iTunesSlider(this.resultList, items);
		} else {
			
			//TODO extract duplicate code
			if (this.resultsClass) {
				Element.addClassName(this.resultList, this.resultsClass);
			}
			
			this.container.appendChild(this.resultList);
		}
		

	},
	
	createResultNode: function(result, position) {
		
		var resultItem = document.createElement('li');
		Element.addClassName(resultItem, 'result-' + position);
		
		var itmsLink = 'itms:'+ result.itemLinkUrl.substring(result.itemLinkUrl.indexOf('//'), result.itemLinkUrl.length);

		var linkUrl = (iTunesCheck()) ? itmsLink : result.itemLinkUrl;
		
		var link = document.createElement('a');
		link.setAttribute('href', linkUrl);

		// album image
		var albumLink = document.createElement('a');
		albumLink.setAttribute('href', linkUrl);
		Element.addClassName(albumLink, 'album');
		 
		var albumImage = document.createElement('img');
		albumImage.setAttribute('src', result.artworkUrl60);
		if (result.itemParentCensoredName) {
			albumImage.setAttribute('alt', result.itemParentCensoredName);
		}
		albumLink.appendChild(albumImage);
		resultItem.appendChild(albumLink);

		var itemTitle = result.itemCensoredName;
		if (result.itemParentCensoredName) {
			itemTitle = result.itemParentCensoredName +': '+ result.itemCensoredName;
		} 

		// title
		var title = document.createElement('a');
		title.setAttribute('href', linkUrl);
		title.setAttribute('title', itemTitle);
		if (result.itemCensoredName.length > 38) {
			title.innerHTML = result.itemCensoredName.substring(0, 38) + '...';
		} else {
			title.innerHTML = result.itemCensoredName;
		}
		resultItem.appendChild(document.createElement('h4').appendChild(title));

		// Artist
		var artist = document.createElement('p');
		Element.addClassName(artist, 'artist');
		if (result.artistName.length > 17) {
			if (result.itemCensoredName.length < 22) {
				artist.innerHTML = result.artistName;
				if (result.artistName.length > 34) {
					artist.innerHTML = result.artistName.substring(0, 34) + '...';
				}
			} else {
				artist.innerHTML = result.artistName.substring(0, 17) + '...';
			}
			artist.title = result.artistName;
		} else {
			artist.innerHTML = result.artistName;
		}
		
		resultItem.appendChild(artist);

		return resultItem;
	},
	
	open: function() {
		this.heading.addClassName('active');
		this.resultList.show();
	},
	
	close: function() {
		this.heading.removeClassName('active');
		this.resultList.hide();
	}

});

Search.Application = Class.create();
Object.extend(Search.Application.prototype, Event.Listener);
Object.extend(Search.Application.prototype, Event.Publisher);
Object.extend(Search.Application.prototype, {

	
	term: null,
	categories: null,
	
	requiredRequests: null,
	optionalRequests: null,
	
	initialize: function(categories) {
		this.categories = categories;
		
		this.requiredRequests = [];	
		this.optionalRequests = [];
	},
	
	getTerm: function(decoded) {
		//TODO remove this intermeditate step, access property directly
		//TODO double check escapedness requirements
		if (decoded) {
			return this.term.replace(/\+/g, ' ');
		} else {
			return this.term;
		}
	},
	
	isolateCategory: function(category) {
		
		if(this.requestTimeout) {
			return;
		}
		
		for ( var catId in this.categories) {
			if (category != this.categories[catId]) {
				this.categories[catId].hide();
			} else if (category){
				//TODO the search app shouldn't be the one to do this
				//but I'm not feeling like creating an isolate method on
				//the base category class, so this seems ok for now
				if (!category.showingAllResults && category.showAvailableResults) {
					category.showAvailableResults();
				}
				category.show();
			}
		}
	},
	
	hideAllCategories: function() {
		for (var catId in this.categories) {
			this.categories[catId].hide();
		}
	},
	
	showAllCategories: function() {
		
		if(this.requestTimeout) {
			return;
		}
		
		this.dispatchEvent('showall', this);
		
		for (var catId in this.categories) {
			var category = this.categories[catId];
			if (category.showOnlyTopResults) {
				category.showOnlyTopResults();
			}
			category.show();
		}
	},

	search: function(term) {
		
		if (typeof(term) === 'undefined' || !term) {
			//TODO throw some error?
			return;
		}
		
		this.term = term; //TODO should term be stored encoded or decoded?
		
		Element.show('loading');
		
		document.title += " for '"+ this.getTerm(true)+"'";
		
		// save the search
		SavedSearch.save(this.getTerm()); //TODO should term be stored encoded or decoded?
		
		this.start_time = (new Date()).getTime();
		// set timeout if the requests don't return anything 
		this.requestTimeout = setTimeout(this.presentResults.bind(this), 10000); //TODO make TTL a settable option
		
		try {
			for (var i = this.requiredRequests.length - 1; i >= 0; i--){
				this.requiredRequests[i].execute();
			}
		} catch(e) {
			//if _any_ required request fails just error immediately
			this.error();
			return;
		}
		
		for (var i = this.optionalRequests.length - 1; i >= 0; i--){
			try {
				this.optionalRequests[i].execute();
			} catch(e) {
				//if an optional request fails just forget about it and move onto the next
				continue;
			}
		}
		
	},
	
	stopRequest: function(request) {
		request.abort(); //TODO may be unnecessary in some cases
		this.requiredRequests = this.requiredRequests.without(request);
		this.optionalRequests = this.optionalRequests.without(request);
	},
	
	stopRequests: function() {
		for (var i=0; i<this.optionalRequests.length; i++) {
			this.stopRequest(this.optionalRequests[i]);
		}
		
		for (var i=0; i<this.requiredRequests.length; i++) {
			this.stopRequest(this.requiredRequests[i]);
		}
	},
	
	hasActiveRequests: function() {
		return (this.requiredRequests.length > 0) || (this.optionalRequests.length > 0);
	},
	
	acknowledgeResponse: function(searchRequest, categories, jumpShortcuts, suggestion) { //TODO this method signature leaves a bit to be desired, too rigid?
		
		try {console.log(searchRequest.getDuration() + "ms " + searchRequest.name);} catch(e) {}
		
		for (var categoryId in categories) {
			
			//ensure we only check for categories we were requesting
			if (this.categories[categoryId]) {
				var categoryResults = categories[categoryId];
				this.categories[categoryId].populate(categoryResults);
			}
		}
		
		if (jumpShortcuts) {
			this.jumpShortcuts = jumpShortcuts;
		}
		
		if (suggestion) {
			this.suggestion = Search.sanitize(suggestion);
		}
		
		this.stopRequest(searchRequest);

		//present results if there are no remaining active requests and
		//the window for displaying results is still open
		if (this.requestTimeout && !this.hasActiveRequests()) {
			this.presentResults();
		}
		
	},
	
	getResultCount: function() {
		
		var resultCount = 0;
		
		for (var catId in this.categories) {
			resultCount += this.categories[catId].totalResultCount;
		}
		
		return resultCount; //TODO candidate for memoization or does this change enough to keep it live?
	},
	
	presentResults: function() {
		
		//close the window for accepting requests
		clearTimeout(this.requestTimeout);
		this.requestTimeout = null;
		
		//TODO show error if we didn't get fullsearch requests back results
		
		Element.hide('loading');
		
		//Rendering the results will take some time so just make sure htye're hidden if we need to
		//actually it's only really an issue in safari tiger, but could 
		//also happen on slower clients where the time to render exceeds or interrupts the 
		//short buildin of the jump results
		if (this.jumpShortcuts) {
			this.hideAllCategories();
		}
		
		if (this.requiredRequests.length > 0) {
			this.error();
			return;
		}
		
		//Present categories
		for (var catId in this.categories) {
			if (this.categories[catId].hasResults()) {
				
				this.categories[catId].render();
				
				this.listenForEvent(this.categories[catId], 'showmore', false, function(evt) {
					var requestedCategory = evt.event_data.data;
					this.isolateCategory(requestedCategory);
				});
				
				this.listenForEvent(this.categories[catId], 'showless', false, this.showAllCategories);

			}
		}
		
		// Results text
		var resultsText = this.getResultCount() + ' Ergebnisse für \'' + this.getTerm(true) + '\'.';
		
		if (this.getResultCount() === 0) {
			resultsText = 'Kein Ergebnis für \'' + this.getTerm(true) + '\'';
		} else if (this.getResultCount() == 1) {
			resultsText = '1 Ergebnis für \'' + this.getTerm(true) + '\'';
		}
		
		$('results').innerHTML = resultsText;
		
		
		this.showSuggestion();
		
		if (this.jumpShortcuts) {
			$('shortcut').innerHTML = this.jumpShortcuts;
			
			Effect.BlindDown('shortcut', {
				duration: 0.75,
				queue: 'front',
				afterFinish: function() {
					this.showAllCategories();
				}.bind(this) });
		} else {
			this.showAllCategories();
		}
		
		this.end_time = (new Date()).getTime();
		this.duration = this.end_time - this.start_time;
		
		// try{ console.log('Duration: ' + this.duration/1000 + 's');}catch(e) {}
	},
	
	showSuggestion: function() {
	
		if (this.suggestion) {
			$('suggestion').innerHTML = 'Meinten Sie <a href="?q=' + encodeURIComponent(this.suggestion).replace(/%2B/g, '+') + '">' + this.suggestion.replace(/\+/g, ' ') + '</a>?';
		}
	},
	
	error: function() {
		
		clearTimeout(this.requestTimeout);
		this.requestTimeout = null;
		
		this.stopRequests();
		Element.hide('loading');
		
		this.showSuggestion();
		
		Element.hide($('search_filters').parentNode);
		$('message').innerHTML = '<p id="error">Bei der Suche trat ein Fehler auf. <a href="?q='+ encodeURIComponent(this.getTerm()).replace(/%2B/g, '+') +'">Bitte wiederholen Sie Ihre Suche</a>.</p>';
		Element.show('message');
	}

});

var Cookie = {
	create: function(name,value)
	{
		var days = 100;
		if (days)
		{
			var date = new Date();
			date.setTime(date.getTime()+(days*24*60*60*1000));
			var expires = '; expires='+date.toGMTString();
		}
		else {
			var expires = '';
		}
		value = value.toString().replace(/[\r|\n|%0D|%0A]/g, '');
		document.cookie = name+'='+value+expires+'; path=/';
	},
	read: function(name)
	{
		var nameEQ = name + '=';
		var ca = document.cookie.split(';');
		for(var i=0;i < ca.length;i++) {
			var c = ca[i];
			
			while (c.charAt(0)==' ') {
				c = c.substring(1,c.length); 
			}
			
			if (c.indexOf(nameEQ) === 0) {
				return c.substring(nameEQ.length,c.length);
			}
		}
		return null;
	},
	erase: function(name)
	{
		var days = -1;
		var value = '';
		var expires = '';
		if (days) {
			var date = new Date();
			date.setTime(date.getTime()+(days*24*60*60*1000));
			expires = '; expires='+date.toGMTString();
		}
		value = value.toString().replace(/[\r|\n|%0D|%0A]/g, '');
		
		
		document.cookie = name+'='+value+expires+'; path=/';
	}
};

/* Feedback Form */
var FeedbackForm = Class.create();
FeedbackForm.prototype = {
	initialize: function(callback, obj) {
		this.callback = callback;
		this.obj = obj;
		this.obj.form = this.obj.getElementsByTagName('form')[0];
	},
	submit: function() {
		// check for values
		
		if ($F('feedback_rating') == '0' && $F('feedback_body') == '') {
			alert('Ihre Meinung ist uns wichtig.');
			return false;
		}
		
		// No need for a response
		var url = this.obj.form.getAttribute('ajaxaction');
		url = "/global/scripts/ajax_proxy.php?s=9&r="+ url + "?" + escape(Form.serialize(this.obj.form));
		try{
			var request = new Ajax.Request(url, {
				method:'get', 
				asynchronous:true, 
				parameters:Form.serialize(this.obj.form)
			});
			
		}catch(e) {
			return;
		}
		Effect.BlindUp(this.obj , {
			duration: 0.4, 
			afterFinish: function() {
				FeedbackForm.thanks(); 
			}});
	}
};

FeedbackForm.thanks = function() {
	$('feedback_form').innerHTML = '<p>Vielen Dank für Ihr Feedback!</p>';
	Effect.BlindDown('feedback_form', {duration: 0.1});
};

/* SavedSearch */
var SavedSearch = Class.create();
SavedSearch.prototype = {
	initialize: function() {
		var num_searches = parseInt(Cookie.read('saved_searches'), 10) || 0;
		this.searches = [];
		for(var i=1;i<=num_searches;i++) {
			this.searches[i] = Cookie.read('saved_searches[' + i + ']');
		}
	},
	render: function(id) {
		var element = $(id);
		if (this.searches.length === 0) {
			element.innerHTML = '<p>Keine gesicherten Suchläufe</p>';
			return false;
		}
		var container = Builder.node('ul');
		for(var i = this.searches.length -1;i>0;i--) {
			var search =
				Builder.node('li', [
					Builder.node('a', { href: '.?q=' + encodeURIComponent(this.searches[i]).replace(/%20/g, '+') }, this.searches[i])
				]);
			container.appendChild(search);
		}
		element.appendChild(container);
	}
};

SavedSearch.save = function(term) {
	var num_searches = parseInt(Cookie.read('saved_searches'), 10);
	if (isNaN(num_searches)) {
		num_searches = 0;
	}
	var searches = [];
	var hasTerm = false;
	var newsearches = [];
	for(var i=1;i<=num_searches;i++) {
		searches[i] = Cookie.read('saved_searches[' + i + ']');
		if (Search.sanitize(searches[i].toLowerCase()) == term.toLowerCase()) {
		 hasTerm = true; 
		}else{
			newsearches.push(searches[i]);
		} 
	}
	
	term = Search.desanitize(term);
	
	if(hasTerm) {
		newsearches.push(term);
		for(var i=1;i<=num_searches;i++) {
			Cookie.create('saved_searches[' + i + ']', newsearches[i-1]);
		}
	}else if(num_searches > 9) {
		newsearches.shift();
		newsearches.push(term);
		for(var i=1;i<=num_searches;i++) {
			Cookie.create('saved_searches[' + i + ']', newsearches[i-1]);
		}
	}else{
		num_searches++;
		Cookie.create('saved_searches', num_searches);
		Cookie.create('saved_searches[' + num_searches + ']', term);
	}
};

SavedSearch.clear = function() {
	Cookie.erase('saved_searches');
	var headers = document.getElementsByTagName('h3', $('savedsearches').parentNode);
	for (var i=0; i<headers.length; i++) {
		if (headers[i].id.match(/savedsearches/)) {
			var header = headers[i];
			var afterEffect = function () { 
				Effect.BlindUp(header, {duration: 0.2}); }.bind(header);
			
			var first = i+1;
		}
	}
	var effect = Effect.BlindUp($('savedsearches'), {duration: 0.1, afterFinish: afterEffect });
	Element.addClassName(headers[first], 'first');
	$('previous-searches').innerHTML = '';
};

