
var g_blnAcDebugEnabled  = false;

var g_arrayAutoCompleter = new Array();
var g_numAutoCompleterId = 0;
var g_numActiveId = 0;

function initAutoCompleter( p_objAcInit )
{
	if ( typeof( p_objAcInit ) == 'object' ) {
		return function() {
			g_arrayAutoCompleter[ g_numAutoCompleterId ] = new AutoCompleter( p_objAcInit, g_numAutoCompleterId );
			g_numAutoCompleterId++;
		};
	} else {
		return function() {};
	}
}


var AutoCompleter = new Class( {
	
	initialize: function( p_objAcInit, p_numId ) {
		
		// Initialize properties with default values
		this.formElement = $(p_objAcInit.formElement);
		this.strURL = p_objAcInit.strURL;
		this.strRequestProxy = null;
		this.strCallback = null;
		this.strCallbackNoResult = null;
		this.strCallbackReset = null;
		this.arrayInfusion = null;
		
		this.numAcItems = 20;
		this.numMinLengthNumeric = 2;
		this.numMinLengthNonNumeric = 3;
		
		this.blnAcContainerShown = false;
		this.numSelectedItem = 0;
		this.lastResponse = null;
		this.fadeDuration = 300;
		this.fullOpacity = 0.95;
		
		this.numLatency = 400;
		this.timerId = false;
		
		
		// Set class instance id
		if ( $defined( p_numId ) ) {
			this.acId = p_numId;
		} else {
			this.acId = 0;
		}
		
		// Set properties according to respective values passed in init object
		if ( $defined( p_objAcInit.arrayInfusion ) ) {
			this.arrayInfusion = p_objAcInit.arrayInfusion;
		}
		if ( $defined( p_objAcInit.strCallback ) ) {
			this.strCallback = p_objAcInit.strCallback;
		}
		if ( $defined( p_objAcInit.strCallbackNoResult ) ) {
			this.strCallbackNoResult = p_objAcInit.strCallbackNoResult;
		}
		if ( $defined( p_objAcInit.strCallbackReset ) ) {
			this.strCallbackReset = p_objAcInit.strCallbackReset;
		}
		if ( $defined( p_objAcInit.numAcItems ) ) {
			this.numAcItems = p_objAcInit.numAcItems;
		}
		if ( $defined( p_objAcInit.numMinLengthNumeric ) ) {
			this.numMinLengthNumeric = p_objAcInit.numMinLengthNumeric;
		}
		if ( $defined( p_objAcInit.numMinLengthNonNumeric ) ) {
			this.numMinLengthNonNumeric = p_objAcInit.numMinLengthNonNumeric;
		}
		if ( $defined( p_objAcInit.strRequestProxy ) ) {
			this.strRequestProxy = p_objAcInit.strRequestProxy
		}
		
		
		this.acAjax = new Ajax();
		this.blnNeedRequest = false;
		
		// Add event to form element
		this.formElement.addEvent( 'keyup', this.processRequestEventPrep.bindWithEvent( this, '' ) );
		this.formElement.addEvent( 'focus', this.processRequestEventPrep.bindWithEvent( this, '' ) );
		
		// Add properties to form element
		this.formElement.setProperty( 'autocomplete', 'off' );
		
		// Add auto-completion container to DOM (if it does not already exist)
		if ( $('AutoCompleteContainer') ) {
			this.acContainer = $('AutoCompleteContainer');
			this.acContainer.addClass( 'AutoCompleteContainer' );
		} else {
			this.acContainer = $(document.createElement( 'div' ));
			this.acContainer.addClass( 'AutoCompleteContainer' );
			$(document.getElementsByTagName( 'body' )[ 0 ]).appendChild( this.acContainer );
		}
		
		// Add event(s) to document
		if ( ! window.opera ) {
			// => only add event when NOT using Opera, because selecting an item with "enter" would send form in Opera
			$(document).addEvent( 'keydown', this.processDocumentKeyEvent.bindWithEvent( this, '' ) );
		}
		$(document).addEvent( 'click', this.processDocumentClickEvent.bindWithEvent( this, '' ) );
		
		// Display debug message container, if debug is enabled
		if ( g_blnAcDebugEnabled && $('AcDebugContainer') ) {
			$('AcDebugContainer').setStyle( 'display', 'block' );
		}
	},
	
	
	// Function is called on "keyup" event from auto-completion form element, starts AJAX request to fetch auto-completion response, after a certain latency
	processRequestEventPrep: function( event, myarg ) {
		if ( $defined( event ) ) {
			// Check for certain keys, which should not trigger a request
			this.debugMessage( 'event.key = ' + event.key );
			if (
					event.key == 'esc' || 
					event.key == 'left' || 
					event.key == 'right' || 
					event.key == 'up' || 
					event.key == 'down' || 
					event.key == 'enter'
			) {
				return false;
			}
		}
		
		g_numActiveId = this.acId;
		if ( this.timerId ) { window.clearTimeout( this.timerId ); }
		this.timerId =
			window.setTimeout( function() {
				g_arrayAutoCompleter[ g_numActiveId ].processRequestEvent();
				g_arrayAutoCompleter[ g_numActiveId ].debugMessage( 'Timer re-triggered processing of request event.' );
			}, this.numLatency );
		
	},
	
	
	// Function is called on "keyup|focus" event from auto-completion form element, starts AJAX request to fetch auto-completion response
	processRequestEvent: function( event, myarg ) {
		if ( this.acAjax && ! this.acAjax.running ) {
			
			var _numMinLength = 0;
			var _strValue = this.formElement.getValue().trim();
			if ( _strValue != '' ) {
				
				if ( isNaN( _strValue ) ) {
					_numMinLength = this.numMinLengthNonNumeric;
				} else {
					_numMinLength = this.numMinLengthNumeric;
				}
				
				if ( _strValue.length >= _numMinLength ) {
					this.debugMessage( 'Starting request, act=' + _strValue + '.' );
					
					if ( $defined( this.strRequestProxy ) ) {
						var _strURLParams = 'act=' + _strValue;
					} else {
						var _strURLParams = 'act=' + escape( _strValue );
					}
					
					// Add infused data to URL parameters
					if ( $defined( this.arrayInfusion ) ) {
						this.arrayInfusion.forEach( function( obj, index ) {
							if ( obj.type == 'static' ) {
								_strURLParams += '&' + obj.name + '=' + obj.value;
							} else {
								_strURLParams += '&' + obj.name + '=' + obj.source.value;
							}
						}, this );
					}
					
					if ( $defined( this.strRequestProxy ) ) {
						var _strRequestURL = this.strRequestProxy + '?url=' + this.strURL + escape( '?' + _strURLParams );
					} else {
						var _strRequestURL = this.strURL + '?' + _strURLParams;
					}
					this.debugMessage( 'Request URL: ' + _strRequestURL );
					
					this.acAjax = new Ajax( _strRequestURL, { method: 'get', onComplete: this.processAcData } );
					this.acAjax.acClassInstance = this;
					this.acAjax.request();
				}
			} else {
				// Empty form element => fade-out auto-completion div (only if it is displayed)
				if ( this.blnAcContainerShown ) {
					var _acFx = new Fx.Style( this.acContainer, 'opacity', { duration: this.fadeDuration, onComplete: this.clearAcContainer } );
					_acFx.acClassInstance = this;
					_acFx.start( this.fullOpacity, 0 );
				}
				
				// Empty form element => call reset function, if defined
				if ( $defined( this.strCallbackReset ) ) {
					var _strOperation = this.strCallbackReset + '();';
					_strOperation = _strOperation.replace( /"/g, "'" );
					eval( _strOperation );
				}
			}
		} else {
			this.blnNeedRequest = true;
			this.debugMessage( 'Need to re-trigger processing of request event.' );
		}
	},
	
	
	// Function is called on "keydown" event from document, selects item in auto-completion div
	processDocumentKeyEvent: function( event, myarg ) {
		event = new Event( event );
		
		if( this.blnAcContainerShown ) {
			
			// Determine number of auto-completion items
			var _numAcItems = this.numAcItems;
			for ( var _i = 0; _i < this.numAcItems; _i++ ) {
				var _id = $('acItem_' + _i);
				if ( $defined( _id ) ) {
					_id.removeClass( 'AcItemHighlight' );
				} else {
					_numAcItems = _i;
					break;
				}
			}
			
			switch ( event.key ) {
				case 'up':
					this.numSelectedItem--;
					if ( this.numSelectedItem < 0 ) {
						this.numSelectedItem = _numAcItems - 1;
					}
					
					var _idPrev = $('acItem_' + this.numSelectedItem);
					if ( $defined( _idPrev ) ) {
						_idPrev.addClass( 'AcItemHighlight' );
					}
					
					event.stop();
					break;
					
				case 'down':
					this.numSelectedItem++;
					if ( this.numSelectedItem >= _numAcItems ) {
						this.numSelectedItem = 0;
					}
					
					var _idNext = $('acItem_' + this.numSelectedItem);
					if ( $defined( _idNext ) ) {
						_idNext.addClass( 'AcItemHighlight' );
					}
					
					event.stop();
					break;
					
				case 'enter':
 					// Insert currently selected auto-completion item into form element or call callback function
					
					var _acData = this.lastResponse;
					var _numSelectedItem = this.numSelectedItem;
					var _strText = this.lastResponse.result_set[ _numSelectedItem ].display_text;
					
					// Prepare operation string
					if ( $defined( this.strCallback ) ) {
						// Replace ' with URL-encoded \' , so nothing will break
						$each( _acData.result_set[ _numSelectedItem ], function( item, key ) {
							if ( typeof( item ) == 'string' ) {
								eval( '_acData.result_set[ _numSelectedItem ].' + key + ' = _acData.result_set[ _numSelectedItem ].' + key + '.replace( /\'/g, "%5C%27" );' );
							}
						} );
						
						var _strOperation = this.strCallback + "(" + Json.toString( _acData.result_set[ _numSelectedItem ] ) + ");";
						_strOperation = _strOperation.replace( /"/g, "'" );
					} else {
						var _strOperation = "document.forms['" + this.formElement.form.name + "'].elements['" + this.formElement.name + "'].value='" + _strText + "';";
					}
					
					// Execute operation string
					eval( _strOperation );
					
					// Empty form element => undisplay auto-completion div (only if it is displayed)
					if ( this.blnAcContainerShown ) {
						this.acContainer.setStyle( 'display', 'none' );
						this.blnAcContainerShown = false;
						this.acContainer.setHTML( '' );
					}
					this.numSelectedItem = 0;
					
					event.stop();
					return false;
					break;
					
				case 'esc':
					this.numSelectedItem = 0;
					
					if ( this.blnAcContainerShown ) {
						var _acFx = new Fx.Style( this.acContainer, 'opacity', { duration: this.fadeDuration, onComplete: this.clearAcContainer } );
						_acFx.acClassInstance = this;
						_acFx.start( this.fullOpacity, 0 );
					}
					
					event.stop();
					return false;
					break;
			}
		}
	},
	
	
	// Function is called on "click" event from document, undisplays auto-completion div
	processDocumentClickEvent: function( event, myarg ) {
		event = new Event( event );
		
		this.numSelectedItem = 0;
		
		if ( this.blnAcContainerShown ) {
			var _acFx = new Fx.Style( this.acContainer, 'opacity', { duration: this.fadeDuration, onComplete: this.clearAcContainer } );
			_acFx.acClassInstance = this;
			_acFx.start( this.fullOpacity, 0 );
		}
	},
	
	
	// Functon is called after fade-out of auto-completion div completed, clears and undisplays auto-completion div
	clearAcContainer: function() {
		// NOTE: "this" is the "Effect object"; "this.acClassInstance" is the "AutoCompleter object"
		
		this.acClassInstance.acContainer.setStyle( 'display', 'none' );
		this.acClassInstance.blnAcContainerShown = false;
		this.acClassInstance.acContainer.setHTML( '' );
	},
	
	
	// Function is called after completed AJAX request, processes response of AJAX request
	processAcData: function( response ) {
		// NOTE: "this" is the "Ajax object"; "this.acClassInstance" is the "AutoCompleter object"
		
		this.acClassInstance.debugMessage( response );
		var response = Json.evaluate( response );
		this.acClassInstance.debugMessage( 'Processing ac response, act=' + response.ac_term + '.' );
		
		this.numSelectedItem = 0;						// Reset currently selected item
		this.acClassInstance.lastResponse = response;	// Store response data
		this.acClassInstance.displayAcData();			// Display stored response data
		
		// Call "no result" callback function (# of results is zero or an error occurred, i.e. result_set is false)
		if ( ( response.number_of_results == 0 || response.result_set == false ) && $defined( this.acClassInstance.strCallbackNoResult ) ) {
			var _strCallbackNoResult = this.acClassInstance.strCallbackNoResult + "();";
			eval( _strCallbackNoResult );
		}
		
		// Send another request by triggering event in form input, if necessary
		// (i.e. another event has happened, when a request was still running)
		if ( this.acClassInstance.blnNeedRequest ) {
			g_numActiveId = this.acClassInstance.acId;
			this.acClassInstance.blnNeedRequest = false;
			window.setTimeout( function() {
				g_arrayAutoCompleter[ g_numActiveId ].processRequestEvent();
				g_arrayAutoCompleter[ g_numActiveId ].debugMessage( 'Re-triggered processing of request event.' );
			}, 200 );
			this.acClassInstance.debugMessage( 'Preparing to re-trigger processing of request event.' );
		}
	},
	
	displayAcData: function() {
		var _formElementDimension = this.formElement.getCoordinates();
		
		// Generate content for auto-completion div
		var _acData = this.lastResponse;
		var _strHTML = '';
		
		if ( _acData.result_set != false ) {
			var _numResults = ( _acData.number_of_results > this.numAcItems ? this.numAcItems : _acData.number_of_results );
			for ( var _i = 0; _i < _numResults; _i++ ) {
				var _strText = _acData.result_set[ _i ].display_text;
				
				if ( $defined( this.strCallback ) ) {
					// Replace ' with URL-encoded \' , so nothing will break
					$each( _acData.result_set[ _i ], function( item, key ) {
						if ( typeof( item ) == 'string' ) {
							eval( '_acData.result_set[ _i ].' + key + ' = _acData.result_set[ _i ].' + key + '.replace( /\'/g, "%5C%27" );' );
						}
					} );
					
					var _strOperation = this.strCallback + "(" + Json.toString( _acData.result_set[ _i ] ) + ");";
					_strOperation = _strOperation.replace( /"/g, "'" );
				} else {
					var _strOperation = "document.forms['" + this.formElement.form.name + "'].elements['" + this.formElement.name + "'].value='" + _strText + "';";
				}
				
				_strHTML += '<div id="acItem_' + _i + '" class="AutoCompleteItem' + ( _i == this.numSelectedItem ? ' AcItemHighlight' : '' ) + '" onclick="' + _strOperation + ' $(this).getParent().setStyle(\'display\',\'none\'); g_arrayAutoCompleter[ g_numActiveId ].blnAcContainerShown=false; g_arrayAutoCompleter[ g_numActiveId ].numSelectedItem=0;" onmouseover="g_arrayAutoCompleter[ g_numActiveId ].uhl(); $(this).addClass(\'AcItemHighlight\'); g_arrayAutoCompleter[ g_numActiveId ].numSelectedItem=' + _i + ';" onmouseout="$(this).removeClass(\'AcItemHighlight\'); g_arrayAutoCompleter[ g_numActiveId ].numSelectedItem=-1;">' + _strText + '</div>';
				_strHTML = unescape( _strHTML ); // Replace URL-encoded \' with respective decoded characters
			}
		}
		
		// Set content and styles of auto-completion div
		if ( _acData.number_of_results > 0 && _acData.result_set != false ) {
			this.acContainer.setHTML( _strHTML );
			this.acContainer.setStyle( 'left', _formElementDimension.left + 1 );
			this.acContainer.setStyle( 'top', _formElementDimension.bottom + 1 );
			
			// Fade-in auto-completion div (only if it is NOT displayed)
			if ( ! this.blnAcContainerShown ) {
				this.acContainer.setStyle( 'opacity', 0 );
				this.acContainer.setStyle( 'display', 'block' );
				this.blnAcContainerShown = true;
				var _acFx = new Fx.Style( this.acContainer, 'opacity', { duration: this.fadeDuration } );
				_acFx.start( 0, this.fullOpacity );
			}
		} else {
			this.acContainer.setHTML( '' );
			this.acContainer.setStyle( 'display', 'none' );
			this.blnAcContainerShown = false;
		}
	},
	
	
	// Function removes "highlight" class from all auto-completion items
	uhl: function() {
		for ( var _i = 0; _i < this.numAcItems; _i++ ) {
			var _id = $('acItem_' + _i);
			if ( $defined( _id ) ) {
				_id.removeClass( 'AcItemHighlight' );
			} else {
				break;
			}
		}
	},
	
	
	// Function writes debug message to container div, if debug mode is enabled via global variable "g_blnAcDebugEnabled"
	debugMessage: function( p_strMessage ) {
		if ( g_blnAcDebugEnabled ) {
			var _date = new Date();
			var _strTime = _date.getHours() + ':' + ( _date.getMinutes() < 10 ? '0' + _date.getMinutes() : _date.getMinutes() ) + ':' + ( _date.getSeconds() < 10 ? '0' + _date.getSeconds() : _date.getSeconds() ) + '.' + _date.getMilliseconds();
			if ( $('AcDebugContainer') ) {
				$('AcDebugContainer').setHTML( $('AcDebugContainer').innerHTML + '<strong>' + _strTime + '</strong>: ' + p_strMessage + '<br />' );
			}
		}
	}
	
} ); // class "AutoCompleter"



