var JsHelpers = {};

/**
 * Creates and returns a delegate function.
 * 
 * @param {Object} theFunction
 * @param {Object} thisObject
 * @param {Object} constantArgs
 */
JsHelpers.create_delegate = function(theFunction, thisObject, constantArgs) {
	// constantArgs must be at least an empty Array
	if (constantArgs == null) {
		constantArgs = new Array();
	}
	return function() {
		var newArgs = new Array();
		for (var iter = 0; iter < constantArgs.length; iter++) {newArgs.push(constantArgs[iter]);}
		for (var iter = 0; iter < arguments.length; iter++) {newArgs.push(arguments[iter]);}
		return theFunction.apply(thisObject, newArgs);
	};
}

// liefert Browser-unabh??ngig ein XMLHttpRequest Objekt zur??ck
JsHelpers.getHTTPObject = function() {
  var xmlhttp;
  /*@cc_on
  @if (@_jscript_version >= 5)
    try {
      xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
    } catch (e) {
      try {
        xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
      } catch (E) {
        xmlhttp = false;
      }
    }
  @else
  xmlhttp = false;
  @end @*/
  if (!xmlhttp && typeof XMLHttpRequest != 'undefined') {
    try {
      xmlhttp = new XMLHttpRequest();
    } catch (e) {
      xmlhttp = false;
    }
  }
  return xmlhttp;
}

JsHelpers.object2queryString = function(ob) {
	var qs = "";
	for (el in ob) {
		if (typeof(ob[el]) == "function")
			continue;
		qs += el + "=";
		if (typeof(ob[el]) == "object") /* should be an array */
			qs += ob[el].join(",");
		else
			qs += ob[el];
		qs += "&";
	}
	
	return qs.substring(0, qs.length - 1);
}

// Fires a HTTP request using the the POST method.
// The request itself is used as first parameter for the specified callback method, followed by the other callback parameters
JsHelpers.requestPOST = function(url, params, callback, callbackParams) {
	var request = JsHelpers.getHTTPObject();
  	request.open("POST", url);
	request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
	request.setRequestHeader("Content-length", params.length);
	request.onreadystatechange = function() {
		// jslog.info(request.readyState);
		if (request.readyState == 4) {
			if (callbackParams == undefined || callbackParams == null)
				callbackParams = [request];
			else
				callbackParams.unshift(request);
			callback.apply(this, callbackParams);
		}
	};
	request.send(JsHelpers.object2queryString(params));
	return request;
}

/** 
  * This class helps you to implement an onkeypress-like behaviour for special keys (like Arrow Left/right/up/down)
  * for the internet explorer, which doesn't fire the onkeypress event for these special keys.
  * Note: Only a single key can be processed simultanuously.
  *
  * @param {Node} target - the html node to which the bahaviour should be added.
  *			Note: if the element already has onkeyup and onkeydown listeners attached they will be executed as well.
  * @param {Function} handler - the handler which should be called repetetively as long as a key is hold down, 
  * 		Note: the handler is NOT called like a regular html event handler: 
  *				  - the this object refers to the html element
  *				  - the keycode of the original event object is passed as argument (in a regular event handler you 
  *					get the whole event object, but this isn't possible because the event object id disposed by IE
  *					right after the onkeydown event has been processed.)
  */
JsHelpers.AddPseudoOnkeypressHandler = function(target, handler) {

	this.target = target;
	this.handler = handler;
	
	// if the target element already has onkeyup and onkeydown listeners we preservce them
	this.targetOnkeyup = this.target.onkeyup;
	this.targetOnkeydown = this.target.onkeydown;
	
	// this is the keycode of the key which was first pressed down
	this.lastKeyCode = null;
	this.workerInterval = null;
	this.pauseTimeout = null;
	
	this._handleDown = function(evt) {
		if (!evt) evt = window.event; // for MSIE
		
		// execute existing onkeydown handler
		if (this.targetOnkeydown != null) this.targetOnkeydown.apply(this.target, [evt]);
		
		// do nothing if any key is already been pressed
		if (this.lastKeyCode != null) return;
		
		this.lastKeyCode = evt.keyCode;
		
		var handlerDelegate = JsHelpers.create_delegate(handler, this.target, [evt.keyCode]);
		
		// call the handler once initially
		handlerDelegate();
		
		// now set the interval to call the handler repetively
		window.clearTimeout(this.pauseTimeout);
		window.clearInterval(this.workerInterval);
		var worker = JsHelpers.create_delegate(
			function() {this.workerInterval = window.setInterval(handlerDelegate, JsHelpers.AddPseudoOnkeypressHandler.INTERVAL);},
			this, null);
		this.pauseTimeout = window.setTimeout(worker, JsHelpers.AddPseudoOnkeypressHandler.START_DELAY); 
	}
	
	this._handleUp = function(evt) {
		if (!evt) evt = window.event; // for MSIE
		
		// execute existing onkeyup handler
		if (this.targetOnkeyup != null) this.targetOnkeyup.apply(this.target, [evt]);
		
		// we're only interested in the single key which was pressed first
		if (evt.keyCode != this.lastKeyCode) return;
		
		// stop the interval
		this.lastKeyCode = null;
		window.clearInterval(this.workerInterval);
		window.clearTimeout(this.pauseTimeout);
	}
	
	// register onkeydown and onkeyup event listeners
	this.target.onkeydown = JsHelpers.create_delegate(this._handleDown, this, null);
	this.target.onkeyup = JsHelpers.create_delegate(this._handleUp, this, null);
}
// these settings normally depend on the system settings, but we choose values which are near the usual defaults
JsHelpers.AddPseudoOnkeypressHandler.START_DELAY = 500;
JsHelpers.AddPseudoOnkeypressHandler.INTERVAL = 40;


/**
 * a simple live search.
 * 
 * Important:
 * set the attribute autocomplete="off" in the search inputfield. Otherwise the browser's assist function and the livesearch
 * will be displayed both to the user.
 *
 * TODO: 
 * - Loader Grafik anzeigen
 * - whitespace in search result ignorieren (momentan muss der Service einfach markup ohne unnötigen whitespace zurückgeben
 *
 * @param {Function} resultRowSelectHandler - function to call when the user presses enter while a result row is selected. 
 *		Note: if the search input field is nested into a form tag and this handler returns TRUE, then the form is submitted as well.
 *		Handler must return false if form shouldn't be submitted.
 */
JsHelpers.LiveSearch = function(seachFieldId, searchResultsId, minInputLength, flags, searchServiceUrl, resultRowSelectHandler) {

	// html nodes
	this.searchField = document.getElementById(seachFieldId);
	this.resultContainer = document.getElementById(searchResultsId);
	this.resultList = null;
	this.closeResultListLink;
	
	// configuration
	this.minInputLength = minInputLength;
	this.flags = flags;
	this.searchServiceUrl = searchServiceUrl;
	this.resultRowSelectHandler = resultRowSelectHandler;
	
	// internal stuff
	this.searchTimeout = null;
	this.currentSearchString = null;
	this.currentRequest = null;
	this.dontHide = false; // this is for dealing with MSIE behaviou which remove the focus from the input field when scrolling in the result list with the mouse
	// indicates whether a valid search result is currently available (this is for the key up/down selection of the search results)
	this.resultAvailable = false;
	this.selectedResultRow;
	
	
	
	/** public interface */
	
	this.init = function() {
		this.searchField.onkeydown = JsHelpers.create_delegate(this._keyDown, this, null);
		this.searchField.onfocus = JsHelpers.create_delegate(this._showSearchResultArea, this, null);
		
		// for the arrow keys doesn't work in MSIE
		// this.searchField.onkeypress = JsHelpers.create_delegate(this._keyPress, this, null);
		
		// note: it is important that onkeyup and onkeydown listeners are added before this pseudo-listener is added
		JsHelpers.AddPseudoOnkeypressHandler(this.searchField, JsHelpers.create_delegate(this._keyPress, this, null));
		
		// we have to wait a bit with hiding, so that the mouse click onto a result row still gets registered
		this.searchField.onblur = JsHelpers.create_delegate(this._delayedHide, this, null);
		
		// this is for MSIE: if the user presses the mouse button while is is over the result area, we won't hide the result list,
		// because we assume he is scrolling in the result list or something
		this.resultContainer.onmousedown = JsHelpers.create_delegate(this._dontHide, this, null);
		
		// add scrollable result row list to result container
		this.resultList = document.createElement("div");
		this.resultList.setAttribute("className", JsHelpers.LiveSearch.SEARCH_RESULT_LIST_STYLE);
		this.resultList.setAttribute("class", JsHelpers.LiveSearch.SEARCH_RESULT_LIST_STYLE);
		this.resultContainer.appendChild(this.resultList);
		
		// add close link to result container
		this.closeResultListLink = document.createElement("div");
		this.closeResultListLink.setAttribute("className", JsHelpers.LiveSearch.CLOSE_BUTTON_STYLE);
		this.closeResultListLink.setAttribute("class", JsHelpers.LiveSearch.CLOSE_BUTTON_STYLE);
		this.closeResultListLink.appendChild(document.createTextNode("schließen"));
		this.closeResultListLink.onclick = JsHelpers.create_delegate(this._hideSearchResultArea, this, null);
		this.resultContainer.appendChild(this.closeResultListLink);
	};
	
	/** sets the given text in the search field */
	this.setText = function(text) {
		this.searchField.value = text;
		// This is for the following use case: the user clicks onto a result row and the text is being transfered into the inputfield.
		// Then the user sets the focus back into the searchfield and types some keys which doesn't change the text. In this case the
		// search shouldn't be triggered again, because the user may browse further through the results.
		this.currentSearchString = this._cleanSearchString(text);
	}
	
	/** returns a string representation of this object */
	this.toString = function() {
		return "livesearch";
	};
	
	
	
	/** private interface */
		
	/** Hides the search result area with a delay of 200ms */
	this._delayedHide = function() {
		if (!this.dontHide)
			window.setTimeout(JsHelpers.create_delegate(this._hideSearchResultArea, this, null), 200);
		this.dontHide = false;
	}
	
	/** sets the flag which prevents hiding */
	this._dontHide = function() {
		this.dontHide = true;
	}
	
	/** called periodically while the user holds down a key while the searchfield has the focus */	
	this._keyPress = function(keyCode) {
		switch (keyCode) {
			case 38: // Up
				this._selectSearchResultRow(-1);
				break;
			case 40: // Down
				this._selectSearchResultRow(1);
				break;
			case 33: // PgUp
				this._selectSearchResultRow(-5);
				break;
			case 34: // PgDown
				this._selectSearchResultRow(5);
				break;
		}
		
		return true; // other event handlers should be executed as well
	}
	
	/** called when the user releases a key while the searchfield has the focus */
	this._keyDown = function(evt) {
		if (!evt) evt = window.event; // for MSIE
		
		// the keyCode property is returned for FF and MSIE
		switch (evt.keyCode) {
			case 13: // Enter
				return this._applySelectedSearchResultRow("kbd"); // we return the result of the handler (so it lies in the logic of the handler whether to continue processing of listeners, e.g. submit of the form)
				break;
			case 27: // ESC
				this._hideSearchResultArea();
				break;
			default: // any other key
				this._scheduleSearch();
				break;
		}
		
		return true; // other event handlers should be executed as well
	}
	
	
	/** hides the search result area. */
	this._hideSearchResultArea = function() {
		this.resultContainer.style.display = "none";
		return true;
	}
	
	/** Shows the search result area only if valid results are available.  */
	this._showSearchResultArea = function() {
		if (this.resultAvailable) {
			this.resultContainer.style.display = "block";
		}
		return true;
	}
	
	
	
	/** Result selection */
	
	/** 
	 * Determines the next result row to select and selects it.
	 * If no result row is selected so far, the first result row is selected by default.
	 */
	this._selectSearchResultRow = function(offset) {	
		// do nothing if currently no valid search result is displayed
		if (!this.resultAvailable) {}
		
		// select the first row if no row is selected so far
		else if (this.selectedResultRow == null) {
			var firstChild = this.resultList.firstChild;
			this._doSelectSearchResultRow(firstChild);
		}
		
		// select the result row by offset
		else {
			var loopElement = this.selectedResultRow;
			var loopElementTmp;
			// var scrollToTop = offset < 0;
			while(offset != 0) {
				if (offset < 0) {
					loopElementTmp = loopElement.previousSibling;
					offset++;
				}
				
				else if (offset > 0) {
					loopElementTmp = loopElement.nextSibling;
					offset--;
				}
				
				if (loopElementTmp != null)
					loopElement = loopElementTmp;
				else
					break;
			}
			
			this._doSelectSearchResultRow(loopElement);
			
			// ensure that the selected row is always visible
			
			// scroll down
			if ((this.selectedResultRow.offsetTop + this.selectedResultRow.offsetHeight) > (this.resultList.offsetHeight +  this.resultList.scrollTop)) {
				var scrollTarget = (this.selectedResultRow.offsetTop + this.selectedResultRow.offsetHeight) - this.resultList.offsetHeight;
				this.resultList.scrollTop = scrollTarget;	
			}
			// scroll up
			else if (this.selectedResultRow.offsetTop < this.resultList.scrollTop) {
				this.resultList.scrollTop = this.selectedResultRow.offsetTop;
			}

			// this causes scrolling to often
			// this.selectedResultRow.scrollIntoView(scrollToTop);
		}
	}
	
	/** Actually selects the given search result row */
	this._doSelectSearchResultRow = function(rowElement) {
		// check if it's a result row
		if (rowElement != null && rowElement.nodeType == 1 && this.closeResultListLink != rowElement) {
			if (this.selectedResultRow != null && this.selectedResultRow.className != null) {
				this.selectedResultRow.className = this.selectedResultRow.className.replace(JsHelpers.LiveSearch.DESELECT_ROW_REGEXP, "");
			}
		
			if (rowElement.className == null) {
				rowElement.className = "";
			}
			rowElement.className += " " + JsHelpers.LiveSearch.SELECTED_ROW_CLASS;
			this.selectedResultRow = rowElement;
		}
	}
	
	/**
	 * Applies the selected search result row.
	 * If no result row is selected or no handler is specified nothing happens.
	 */
	this._applySelectedSearchResultRow = function(source) {
		if (this.selectedResultRow == null || this.resultRowSelectHandler == null) return true;
		// if the handler returns true we hide the results area
		if (this.resultRowSelectHandler.apply(document, [this, this.selectedResultRow, source])) {
			this._hideSearchResultArea();
			return true;
		}
		return false;
	}
	
	
	
	/** Search workflow */
	
	/** schedules the search task */
	this._scheduleSearch = function() {
		window.clearTimeout(this.searchTimeout);
		this.searchTimeout = window.setTimeout(JsHelpers.create_delegate(this._doSearch, this, null), JsHelpers.LiveSearch.REQUEST_DELAY);
	};
	
	/** Actually executes the search if the provided search string is sufficient. */
	this._doSearch = function() {
		var newSearchString = this._cleanSearchString(this.searchField.value);
		if (newSearchString == "") {
			this._cancelRequest(); // cancel current running search
			this._invalidateSearchResult();
			this.resultList.innerHTML = "";
			this._hideSearchResultArea();
		} else if (this.minInputLength != null && newSearchString.length < this.minInputLength) {
			this._cancelRequest(); // cancel current running search
			this._invalidateSearchResult();
			this.resultList.innerHTML = "";
			this._hideSearchResultArea();
		} else if (this.currentSearchString == newSearchString) {
			// wait for the response of the current running search
		} else {
			this._cancelRequest(); // cancel current running search
			this._invalidateSearchResult();
			this.currentRequest = JsHelpers.requestPOST(this.searchServiceUrl, {search: newSearchString}, JsHelpers.create_delegate(this.onResult, this, null), []); // start new search request
		}
		
		this.currentSearchString = newSearchString;
	};
	
	/** Cancels the current running http search request if any. This is used when a request has been sent, but the user keeps on typing. */
	this._cancelRequest = function() {
		if (this.currentRequest != null) {
			this.currentRequest.abort();
			this.currentRequest = null;
		}
	};
	
	/** invalidates the current displayed live search results. */
	this._invalidateSearchResult = function() {
		this.selectedResultRow = null;
		this.resultAvailable = false;
		// avoid flickering
		// this.resultList.innerHTML = "";
		// this._hideSearchResultArea();
	}
	
	/** Is being called when the response of the search request is available */
	this.onResult = function(theRequest) {
		// do nothing if request failed
		if (theRequest.status != 200) return;
		
		// get status row from result
		var resultStatus = theRequest.responseText.substring(0, theRequest.responseText.indexOf("\n"));
		var resultStatusParts = resultStatus.split(":");
		if (resultStatusParts[0] == "ILLEGAL_SEARCHSTRING") {
			this.resultList.innerHTML = "";
			this._hideSearchResultArea();
			return;
		}
		if (resultStatusParts[0] == "RESULT_COUNT") {
			// if no results do nothing
			// TODO: Meldung an den User
			if (parseInt(resultStatusParts[1]) == 0) {
				this.resultList.innerHTML = "";
				this._hideSearchResultArea();
				return;
			}
		}
		
		var resultBody = theRequest.responseText.substring(theRequest.responseText.indexOf("\n") + 1);	
		this.resultList.innerHTML = resultBody;
		this.resultAvailable = true;
		this._showSearchResultArea();
		this.resultList.scrollTop=0;
		
		// we have to wait till next thread until the html is renderer
		window.setTimeout(JsHelpers.create_delegate(this._postProcessResult, this, null), 0);
	};
	
	/** is being called after the new results have been rendered to the results area */
	this._postProcessResult = function() {
		/* register the apply listener with mouse release for each result row */
		if (this.resultRowSelectHandler != null && this.resultAvailable) {
			var resultRow = this.resultList.firstChild;
			while (resultRow != null) {
				resultRow.onmouseup = JsHelpers.create_delegate(this._selectSearchResultRowByMouse, this, [resultRow]);
				resultRow = resultRow.nextSibling;
			}
		}
	}
	
	/** selects the given resultRow */
	this._selectSearchResultRowByMouse = function(resultRow) {
		this.dontHide = false; // for MSIE scrolling removes focus from search inputfield (this is called on release, the flag is set before on mouse down)
		this._doSelectSearchResultRow(resultRow);
		this._applySelectedSearchResultRow("mouse");
	}
	
	
	/** Helper methods */
	
	this._cleanSearchString = function(str) {
		if ((this.flags & JsHelpers.LiveSearch.CASE_INSENSITIVE) > 0)
			str = this.searchField.value.toLowerCase();
		
		// strip blanks from the beginning and the end
		if ((this.flags & JsHelpers.LiveSearch.TRIM) > 0) {
			while (str.length > 0 && str.charAt(0) == " ") {str = str.substr(1);}
			while (str.length > 0 && str.charAt(str.length-1) == " ") {str = str.substr(0, str.length-1);}
		}
		
		// strip blanks from the beginning and the end
		if ((this.flags & JsHelpers.LiveSearch.CONDENSE_BLANKS) > 0) {
			str = str.replace(/\W{2,}/g, " ");
		}
		
		return str;
	};
}

JsHelpers.LiveSearch.CASE_INSENSITIVE = 1;
JsHelpers.LiveSearch.TRIM = 2;
JsHelpers.LiveSearch.CONDENSE_BLANKS = 4;
JsHelpers.LiveSearch.SELECTED_ROW_CLASS = "liveSearch_selectedRow";
JsHelpers.LiveSearch.DESELECT_ROW_REGEXP = / ?liveSearch_selectedRow/;
JsHelpers.LiveSearch.REQUEST_DELAY = 50;
JsHelpers.LiveSearch.SEARCH_RESULT_LIST_STYLE = "liveSearch_searchResultList";
JsHelpers.LiveSearch.CLOSE_BUTTON_STYLE = "liveSearch_searchResultClose";