/**
  * Request object; encapsulates the data required to
  * make an AJAX request.
  */
function Request(sMethod, sQueryString, sUrl, oCallback) {
  /* member variables */
  this.sUrl = sUrl;
  this.sQueryString = sQueryString;
  this.sMethod = sMethod;
  this.oCallback = oCallback;
};

	/* Class to manage the autocomplete functionality
	 * of a textbox.  The function uses the text typed
	 * into a textfield to search for results held on
	 * a remote server using AJAX requests.  The results
	 * are rendered into a list of items that can be 
	 * clicked to quickly complete the text field.
	 * The response from the AJAX request must be a
	 * series of div elements, each div represents an
	 * individual list item.
	 */
	DynamicMatch = Class.create();
	DynamicMatch.prototype = {
		/* Initialise class member fields and attach
		 * event handlers to the appropriate text field. 
		 * 
		 * Input Parameters:
		 * sFieldId 	- id of the text field element to attach functionality to.
		 * sListId		- id of the div element to render list of suggestions in.
		 * sUrl			- Relative URL that the AJAX request will forward to.
		 * sQueryString	- Query string that AJAX will append text field data to.
		 */
		initialize: function (sFieldId, sListId, sUrl, sQueryString){
			/* Variables related to AJAX request. */
			this.sUrl = sUrl;
			this.sQueryString = sQueryString;
			this.iRequestId = null;
			/* Track when control has focus */
			this.bHasFocus = false;
			/* variables related to list. */
			this.aListItems = null;
			this.bFieldSet = false;
			this.iMaxDisplayed = 10;
			this.sLastValue = "";
			this.iPreviousIndex = -1;
			this.iCurrentIndex = -1;
			/* Variables related to caches. */
			this.aSelectedCache = new Array();
			this.aAjaxCache = new Array();
			this.iLastAjaxCacheIndex = -1;
			/* Get the elements we are managing. */
			this.oField = document.getElementById(sFieldId);
			this.oList  = document.getElementById(sListId);
			/* Attach event handlers to text field. */
			Event.observe(this.oField, 'focus', 
this.fieldGotFocus.bindAsEventListener(this), false);
			Event.observe(this.oField, 'keyup', 
this.processKeyPress.bindAsEventListener(this), false);
			Event.observe(this.oField, 'blur', 
this.fieldLostFocus.bindAsEventListener(this), false);
		},

		/* This method deals with displaying the list of items
		 * when the text field gets focus.
		 */
		fieldGotFocus: function(oEvent){
			this.bHasFocus = true;
			if (!this.bFieldSet && this.aListItems != null && this.aListItems.length 
 > 0){
				this.oList.style.display = "block";
			}
		},

		/* This method deals with removing the list of items
		 * when the text field loses focus.
		 */
	    fieldLostFocus: function (oEvent) {
	        this.sLastValue = "";
	        this.bHasFocus = false;
	        this.closeList();
	    },

		/* Closes the list and resets the list position trackers.*/
		closeList : function () {
			// reset indexes
			this.iCurrentIndex = -1;
			this.iPreviousIndex = this.iCurrentIndex;
			this.oList.style.display = "none";
		},

		/* This method deals with handling events fired in
		 * response to a key being pressed.  The following
		 * keys are monitored:
		 * ESCAPE - Closes list and resets text field value.
		 * RETURN - Closes list and sets text field to highlighted option.
		 * UP     - Moves highlight up to next list item.
		 * DOWN   - Moves highlight down to next list item.
	     */
		processKeyPress : function(oEvent){
			switch (oEvent.keyCode){
				case Event.KEY_ESC:
					this.oField.value = this.sLastValue;
					this.closeList();
					return;
					break;
				case Event.KEY_RETURN:
					this.bFieldSet = true;
					this.closeList();
					this.addSelectedToCache(this.getListItemNode());
					return;
					break;
				case Event.KEY_UP:
					this.iCurrentIndex--;
					this.updateListFocus();
					return;
					break;
				case Event.KEY_DOWN:
					this.iCurrentIndex++;
					this.updateListFocus();
					return;
					break;
			}
			// Try getting data from server
			this.doAjaxRequest();
		},

		/* Attach event handlers to each list item node.
		 * Events handled are mousedown and mouseover.
		 */
		addListItemListeners : function(){
			for (var i=0; i<this.aListItems.length; i++){
				var node = this.aListItems[i];
	            node.onmousedown = 
this.mouseClickListItem.bindAsEventListener(this);
	            node.onmouseover = 
this.mouseHighlightListItem.bindAsEventListener(this);
			}
		},

		/* Event handler for the mouseclick event, this 
		 * is fired when the user clicks the mouse when
		 * it is positioned over a list item.
		 */
		mouseClickListItem : function(oEvent){
			var oListItem = Event.element(oEvent);
			var sLocation = oListItem.getAttribute("location");
			if (!sLocation){
				// sLocation is not on the element we checked, try its parent
				oListItem = oListItem.parentNode;
				sLocation = oListItem.getAttribute("location");
			}
			sLocation = oListItem.getAttribute("location");

			// set the value of the text field
			this.sLastValue = sLocation;
			this.oField.value = sLocation;
			this.bFieldSet = true;
			this.addSelectedToCache(oListItem);
		},

		/* Event handler for the mouseover event, this
		 * is fired when the user positions the mouse
		 * over a list item.
		 */
		mouseHighlightListItem : function(oEvent){
			var oListItem = Event.element(oEvent);
			var sLocation = oListItem.getAttribute("location");
			if (!sLocation){
				// sLocation is not on the element we checked, try its parent
				oListItem = oListItem.parentNode;
				sLocation = oListItem.getAttribute("location");
			}
			// Get the index of the selected list item
			this.iCurrentIndex = this.aListItems.indexOf(oListItem);
			this.updateListFocus();
		},

		/* This method deals with updating the styles of the 
		 * list items, such that the highlighted item is displayed
		 * with a different style to the other list items.
		 */
		updateListFocus : function(){
			/* check index is within array range
			 * (starts at -1 and array size may have changed since last call) */
			if (this.iPreviousIndex >= 0 && 
this.iPreviousIndex<this.aListItems.length){
				this.aListItems[this.iPreviousIndex].className = "normalListItem";
			}

			/* Reset the current index if less than zero. */
			if (this.iCurrentIndex <0){
				this.iCurrentIndex = -1;
			}

			/* Reset the current index if greater than list length */
			if (this.iCurrentIndex >= this.aListItems.length){
				this.iCurrentIndex = this.aListItems.length - 1;
			}

			/* Deals with updating the styling of the selected list item
			 * and setting the text field value. */
			if (this.iCurrentIndex >= 0 && 
this.iCurrentIndex<=this.aListItems.length){
				/* set the style of the selected list item */
				this.aListItems[this.iCurrentIndex].className = "highlightListItem";
				var oListItem = this.aListItems[this.iCurrentIndex];
				var sLocation = oListItem.getAttribute("location");
				if (!sLocation){
					sLocation = oListItem.parentNode.getAttribute("location");
				}
				this.oField.value = sLocation;
			}

			/* update the previous index to be the current index */
			this.iPreviousIndex = this.iCurrentIndex;
		},

		/* This method returns the list item that is currently selected. */
		getListItemNode : function(){
			var sSelectedLocation = this.oField.value;
			for (var i=0; i<this.oList.childNodes.length; i++){
				if (this.oList.childNodes[i].getAttribute("location") == 
sSelectedLocation){
					return this.oList.childNodes[i];
				}
			}
			return null;
		},

		/* Renders the list of items from a XHTML fragment,
		 * passed as a string.  Each list item must be defined
		 * inside a DIV element.  No other DIV elements can be
		 * present in the XHTML fragment.
		 * */
		renderListFromText : function (sListText) {
			// Clear previous list entries
			while (this.oList.firstChild){
				this.oList.removeChild(this.oList.firstChild);
			}

			/* Create a temporary div element to use to transform 
			 * sListText into an arrary of DOM elements. */
			var oTempDiv = document.createElement("div");
			oTempDiv.innerHTML = sListText;
			this.aListItems = $A(oTempDiv.getElementsByTagName("div"));

			/* deal with no matches case. */
			if (this.aListItems.length == 0)
			{
				this.closeList();
				return;
			}

			/* Trim array size to maximum displayed. */
			if (this.aListItems.length > this.iMaxDisplayed){
				this.aListItems = this.aListItems.slice(0,this.iMaxDisplayed);
			}
			
			/* Append children to displayed list. */
			for (var i=0; i<this.aListItems.length; i++){
				this.oList.appendChild(this.aListItems[i]);
			}

			/* add list item event listeners */
			this.addListItemListeners();

			/* Get absolute position of the text field. */
			var aOffsets = this.getAbsolutePositionOffset(this.oField);
			var leftPos = aOffsets[0];
			var topPos  = aOffsets[1] + this.oField.offsetHeight;
			
			/* set list box CSS style properties. */
			this.oList.className ="matchedLocationList";
			this.oList.style.left = leftPos+"px";
			this.oList.style.top = topPos+"px";
			
			/* Check text field has focus before displaying. */		
			if (this.bHasFocus)
			{
				this.oList.style.display = "block";				
			}
		},
	
	/*
	 * Gets the left and top offsets of the passed
	 * element from its nearest containing block
	 * that is positioned.
		 *
		 * deprecated : Use version from /uk/co/aimltd/resources/javascript/DOMFunctions.js      
	 */
    getAbsolutePositionOffset : function(oElement){
    	/* Initialize the offset variables to zero. */
		var iOffsetLeft = iOffsetTop = 0;
		if (oElement.offsetParent)
      		{
	      	/* Get the offsets of the element. */
    	    iOffsetLeft = oElement.offsetLeft;
        	iOffsetTop = oElement.offsetTop;
	        while(oElement=oElement.offsetParent)
    	    	{
        		/* Offset should be stopped when a
        		 * positioned block is encountered. */
				if (this.isElementPositionedAbsolute(oElement)
    	      		|| this.isElementPositionedRelative(oElement))
        	  		{
          			/* Stop adding further offsets. */
	            	break;
					}     
				/* Add this element's offsets to current value. */
				iOffsetLeft += oElement.offsetLeft;
				iOffsetTop  += oElement.offsetTop;   
				}
			}
		/* Return the calculated offsets. */
		return [iOffsetLeft, iOffsetTop];
    },
    
    /*
     * Determines if the passed element is absolutely positioned.
		 *
		 * deprecated : Use version from /uk/co/aimltd/resources/javascript/DOMFunctions.js      
     */
    isElementPositionedAbsolute: function (oElement){
    	/* Initialize the variables. */
		var bIsAbsolute = false; 
		var oCurrentStyle = this.getCurrentStyle(oElement);

		/* Have we populated the oCurrentStyle object? */	
		if (oCurrentStyle != null)
			{
			/* Is the element absolutely positioned? */
			bIsAbsolute = (oCurrentStyle.position.toLowerCase() == "Absolute".toLowerCase())?true:false;
		}
		/* Return true is absolutely positioned, false otherwise. */
		return bIsAbsolute;
    },
    
    /*
     * Determines if the passed element is relatively positioned.
		 *
		 * deprecated : Use version from /uk/co/aimltd/resources/javascript/DOMFunctions.js      
     */
    isElementPositionedRelative: function (oElement){
    	/* Initialize the variables. */
		var bIsRelative = false; 
		var oCurrentStyle = this.getCurrentStyle(oElement);

		/* Have we populated the oCurrentStyle object? */	
		if (oCurrentStyle != null)
			{
			/* Is the element absolutely positioned? */
			bIsRelative = (oCurrentStyle.position.toLowerCase() == "Relative".toLowerCase())?true:false;
		}
		/* Return true is absolutely positioned, false otherwise. */
		return bIsRelative;
    },   
    
    /*
     * Determines if the passed element is absolutely positioned.
		 *
		 * deprecated : Use version from /uk/co/aimltd/resources/javascript/DOMFunctions.js      
     */
    getCurrentStyle: function (oElement){
    	/* Initialize the variables. */
		var oCurrentStyle = null;

		/* Object detection for DOM browsers. */
		if (oElement.currentStyle) 
			{
			/* For DOM Compliant Browsers. */
			oCurrentStyle = oElement.currentStyle;
      		}
		else if (document.defaultView)
			{
			/* For IE. */
			oCurrentStyle = document.defaultView.getComputedStyle(oElement,null);
			}

		/* Return the current style if available. */
		return oCurrentStyle;
    },    
    		
		/* Get absolute position of object.  Use this
		 * function to position elements absolute on the 
		 * screen.  These positions will render the element
		 * in the same position on Firefox and IE. 
		 * This function returns the absolute position of 
		 * the element with respect to its upper most 
		 * container.
		 *
		 * deprecated : Use version from /uk/co/aimltd/resources/javascript/DOMFunctions.js      
		 */		
		getAbsoluteOffsetsLame : function (oElement){
			/* Set up the variables */
			var iOffsetLeft = iOffsetTop = 0;
			/* Does element have a parent offset/ */
			var aOffsetLeft = new Array();
			var aOffsetTop  = new Array();
			
			if (oElement.offsetParent){
				aOffsetLeft[0] = oElement.offsetLeft;
				aOffsetTop [0] = oElement.offsetTop;	
				var index = 1;		
				while (oElement = oElement.offsetParent){
					aOffsetLeft[index] = oElement.offsetLeft;
					aOffsetTop [index] = oElement.offsetTop;
					index++;				
				}
				/* Ignore last two offsets */
				for (var i=0; i<index-2; i++)
				{
				iOffsetLeft += aOffsetLeft[i];
				iOffsetTop  += aOffsetTop[i];									
				}
			}
			return [iOffsetLeft, iOffsetTop];
		},
				
		/* Get absolute position of object.  Use this
		 * function to position elements absolute on the 
		 * screen.  These positions will render the element
		 * in the same position on Firefox and IE. 
		 * This function returns the absolute position of 
		 * the element with respect to its upper most 
		 * container.
		 *
		 * deprecated : Use version from /uk/co/aimltd/resources/javascript/DOMFunctions.js      
		 */		
		getAbsoluteOffsetsOriginal : function (oElement){
			/* Set up the variables */
			var iOffsetLeft = iOffsetTop = iLastLeft = iLastTop = 0;
			/* Does element have a parent offset/ */
			if (oElement.offsetParent){
				iOffsetLeft = oElement.offsetLeft;
				iOffsetTop = oElement.offsetTop;			
				while (oElement = oElement.offsetParent){
					iOffsetLeft += oElement.offsetLeft;
					iOffsetTop  += oElement.offsetTop;				
				}
			}
			return [iOffsetLeft, iOffsetTop];
		},
		
		/* Adds user selected item to the selected items cache. */
		addSelectedToCache : function(oListItem) {
			if (oListItem){
				this.aSelectedCache.push(oListItem);
			}
		},

		/* Returns an array of all cached data, from AJAX request
		 * and from user selections.
		 */
		getCachedData : function() {
			var aCachedData = new Array();
			aCachedData = aCachedData.concat(this.aAjaxCache);
			aCachedData = aCachedData.concat(this.aSelectedCache);
			return aCachedData;
		},

		/* Searches for matches to the Regular Expression passed as
		 * reToMatch in the array of locations aLocations.  
		 * Returns an array with two elements:
		 * Element[0] - Array of matched locations.
		 * Element[1] - Index of last match.
		 */
		findMatches : function(reToMatch, aLocations){
			var aMatchedLocations = new Array();
			var aLastMatchedIndex = -1;
				var iIndex = 0;
				for (var i=0; i<aLocations.length; i++){
					if (reToMatch.test(aLocations[i].getAttribute("location")))
					{
						aMatchedLocations[iIndex] = aLocations[i];
						aLastMatchedIndex = i;
						iIndex++;
					}
					/* No need to continue past the max items in list */
					if (aMatchedLocations.length-1 == this.iMaxDisplayed){
						return [aMatchedLocations, aLastMatchedIndex];
					}
				}
			return [aMatchedLocations, aLastMatchedIndex];
		},

		/*  Get locations from the available caches that match the
		 * currently typed input.
		 */
		getMatchedLocations : function () {
			var sLocation = this.oField.value;
			var aMatchedLocations = new Array();
			if (sLocation != ""){
				var reFindMatches = new RegExp("^"+sLocation,"i");
				//var aLocations = this.getCachedData();
				var aAjaxResults = this.findMatches(reFindMatches, this.aAjaxCache);
				var aSelectedResults = this.findMatches(reFindMatches, 
this.aSelectedCache);

				this.iLastAjaxCacheIndex = aAjaxResults[1];
				aMatchedLocations = aMatchedLocations.concat(aAjaxResults[0]);
				aMatchedLocations = aMatchedLocations.concat(aSelectedResults[0]);
			}
			return aMatchedLocations;
		},

		/* Are there matches in the cache for current typed text? */
		hasMatchesInLocalCache : function() {
			var aMatches = this.getMatchedLocations();
			// check data for matches
			if (aMatches.length > this.iMaxDisplayed){
				return true;
			}
			else
			if (aMatches.length > 0 &&
			this.iLastAjaxCacheIndex > -1  &&
			this.iLastAjaxCacheIndex < this.aAjaxCache.length-1) {
				/* Have found matches and not at end of the cache */
				return true;
			}
			return false;
		},

		/* Render list from cached data. */
		renderListFromCache : function() {
			var aMatchedLocations = this.getMatchedLocations();
			this.renderListItems(aMatchedLocations);
		},

		/* Process the AJAX request object that is populated
		 * with the AJAX response data.
		 */
		processAjaxResponse : function(oAjaxRequest) {
			/* Get the responseText from the AJAX Request object. */
			var	sResponseText = oAjaxRequest.responseText;
			this.renderListFromText(sResponseText);
		},

		/*
		 * Temporary method for testing.
		 */
		createResponseText : function(aMatches){
			/* var sExampleResponse = "<div class=\"normalListItem\"
			 * location=\"/nz/grey/greymouth/l+a+place/\">
			 * <span style=\"font-weight: bold;\">L A Place</span>,
			 * Greymouth, Grey</div>"; */
			var oInnerHTML = document.createDocumentFragment();
			var iLoopLength = (aMatches.length < this.iMaxDisplayed)? aMatches.length 
: this.iMaxDisplayed;
			for (var i=0; i<iLoopLength; i++){
				var oDiv = document.createElement("div");
				oDiv.setAttribute("location", aMatches[i]);
				oDiv.className = "normalListItem";
				var oSpan = document.createElement("span");
				oSpan.className = "locationOption";
				oSpan.appendChild(document.createTextNode(aMatches[i]));
				oDiv.appendChild(oSpan);
				oDiv.appendChild(document.createTextNode(", [address]"));
				oInnerHTML.appendChild(oDiv);
			}
			// remove old entries from this.oList
			while (this.oList.firstChild){
				this.oList.removeChild(this.oList.firstChild);
			}
			this.oList.appendChild(oInnerHTML);
			return this.oList.innerHTML;
		},

		/* Remove any HTTP special characters from the search string.
		 */
	    sanitizeQuery: function (query) {
    	    var sanitized_query = query;
        	sanitized_query = sanitized_query.replace(/[\&]/g,'');
	        return escape(sanitized_query);
    	},
    	
		/* This method deals with making a remote request for
		 * matches to the currenly typed text.  
		 */
		doAjaxRequest : function() {
			// check search string is valid
			var sSearchText = this.oField.value;
			if (sSearchText == ""){
				// no search text
				this.closeList() ;
				return;
			}

			// check that field string has changed
			if (sSearchText == this.sLastValue){
				return;
			}
			// set last value to current edit
			this.sLastValue = sSearchText;

			// text field string is modified
			this.bFieldSet = false;

			// check local cache for matches
			if (this.hasMatchesInLocalCache()){
				// update the list with matched data from cache
				this.renderListFromCache();
				return;
			}

			// Cancel a queued request
			if (this.iRequestId != null){
				//RequestHandler.cancelRequest(this.iRequestId);
				window.clearTimeout(this.iRequestId);
				this.iRequestId = null;
			}

			// need to send an AJAX request 
			var url = this.sUrl;
	        var sParams = this.sQueryString + this.sanitizeQuery(sSearchText);				
			var sMethod = "get";
			var oCallback = this.processAjaxResponse.bind(this);
			var oRequest = new Request(sMethod, sParams, this.sUrl, oCallback);
			
			var queuedRequest = function(){
		        var url = oRequest.sUrl;
        		//var sQueryString = this.sanitizeQuery(oRequest.sQueryString);
		        var sQueryString = oRequest.sQueryString;
        		var ajax_request = new Ajax.Request(url, {
                	method: oRequest.sMethod,
	                parameters: sQueryString,
    	            onComplete: oRequest.oCallback
        		    }
		        );					
			}
			// only send request after a 2 second pause
			this.iRequestId = window.setTimeout(queuedRequest, 2000);   
			
			// only send request after a 2 second pause - FAILS ON IE
			//this.iRequestId = RequestHandler.queueRequest(oRequest, 2000);
		}
	};
	


