function searchInput(elementId, config){

	var defaultOptions = {
		containerId: '',
		activeClass: 'active',
		limit: 10,
		minKeys: 0,
		dataType: 'json',
		q: '',
		url: '',
		onResults: false,
		onError: false
	};

	if(config){
		defaultOptions = $.extend(defaultOptions, config);
	}
	
	var KEY = {
		UP: 38,
		DOWN: 40,
		DEL: 46,
		TAB: 9,
		RETURN: 13,
		ESC: 27,
		COMMA: 188,
		PAGEUP: 33,
		PAGEDOWN: 34,
		BACKSPACE: 8
	};

	var mouseOver = false;
	var currentPos = -1;
	var $input = $('#'+ elementId).attr("autocomplete", "off");
	var $container = $('#'+ defaultOptions.containerId).mouseover(function(){
		mouseOver = true
	}).mouseout(function(){
		mouseOver = false;
	});

	var requestId = 0;
	var lastRequestId = 0;
	var timeout;
	var blockSubmit;
	
	var cache = $.icAutocompleter.Cache({
		cacheLength: 1000
	});

	// prevent form submit in opera when selecting with return key
	$.browser.opera && $($input.form).bind("submit", function() {
		if (blockSubmit) {
			blockSubmit = false;
			return false;
		}
	});

	$input.bind(($.browser.opera ? "keypress" : "keydown"), function(event){

		switch(event.keyCode) {

			case KEY.UP:
				event.preventDefault();
				if ( visible() ) {
					prevItem();
				} 
				return false;

			case KEY.DOWN:
				event.preventDefault();
				if ( visible() ) {
					nextItem();
				}else{					
					onChange( );	
				}
				return false;

			case KEY.PAGEUP:
				event.preventDefault();
				if ( visible() ) {
					//pageUp();
				}
				return false;

			case KEY.PAGEDOWN:
				event.preventDefault();
				if ( visible() ) {
					//select.pageDown();
				}
				return false;

			case KEY.TAB:
			case KEY.RETURN:
				if( selectCurrent() ) {
					// stop default to prevent a form submit, Opera needs special handling
					event.preventDefault();
					blockSubmit = true;
					return false;
				}
				return true;

			case KEY.ESC:
				hideAutocomplete();
				return false;

			default:				
				clearTimeout(timeout);
				timeout = setTimeout(onChange, 1);
			
		}
	})
	.blur(function(event){
		if(!mouseOver){
			hideAutocomplete();
		}
	});

	function onChange(){
		
		var reqId = ++requestId;

		var phrase = $.trim( $input.val() );
		var cached = cache.load(phrase);

		if(cached){
			onResults(cached, reqId);
			return;
		}

		if( phrase && defaultOptions.minKeys < phrase.length ){

			$('#jq_tooltipKeyword').text(phrase);

			$.ajax({
				mode: "abort",
				url: defaultOptions.url,
				data: {
					q: phrase,
					limit: defaultOptions.limit
				},
				dataType: defaultOptions.dataType,
				success: (typeof( defaultOptions.onResults ) == 'function' ? defaultOptions.onResults : function(data){					
					cache.add(phrase, data);
					onResults(data, reqId);					
				}),
				error: (typeof( defaultOptions.onError ) == 'function' ? defaultOptions.onError : onError)
			});

		}else{
			hideAutocomplete();
			clearList();
		}

	}

	function onResults(data, reqId){

		if(lastRequestId > reqId){
			return false;
		}

		lastRequestId = reqId;
		
		if(!data || !data.list){
			hideAutocomplete();
			clearList();
			return false;			
		}

		fillList(data);
		showAutocomplete();
	}

	function onError()	
	{
	}

	function hideAutocomplete(){
		$container.hide();
	}

	function showAutocomplete(){
		$container.show();		
	}

	function fillList(data){
		currentPos = -1;
		$('#jq_tooltiplist').html(data.list);
		$('#jq_tooltiplist li').mouseover(function(e){			
			currentPos = $(this).attr('id').replace(/jq_searchItem_/i, '');
			highlight();
		})
		.click(function(ev){
			$input.val( $(this).attr('title') ).focus();
			hideAutocomplete();		
			clearList();
		});
		$('#jq_tooltipItems').html(data.items);
		highlightOnResult();
		//highlight();
	}

	function clearList(){
		currentPos = -1;
		$('#jq_tooltiplist').html('');
		$('#jq_tooltiplist li').unbind();
		$('#jq_tooltipItems').html('');
	}

	function visible(){
		return $container && $container.is(":visible");
	}

	function getElementsCount(){
		return $('#jq_tooltiplist li').length;
	}

	function highlightItem(move){

		var count = getElementsCount();	

		if(!count) return false;

		currentPos += (move);

		if(move > 0 && currentPos >= count){
			currentPos = 0;	
		}

		if(move < 0 && currentPos < 0){
			currentPos = count-1;
		}

		highlight();

	}
	
	function highlight(){		
		currentPos = parseInt(currentPos);		
		$('#jq_tooltiplist li').removeClass(defaultOptions.activeClass);
		$('#jq_searchItem_'+currentPos).addClass(defaultOptions.activeClass);
		$('.jq_searchRes').hide();
		$('#jq_searchRes_'+currentPos).show();
	}
	
	function highlightOnResult(){
		var prevPos = currentPos;
		currentPos = 0;
		$('.jq_searchRes').hide();
		$('#jq_searchRes_'+currentPos).show();
		currentPos = prevPos;
	}

	function nextItem(){
		highlightItem(1);
	}

	function prevItem(){
		highlightItem(-1);
	}

	function selectCurrent(){

		if(!visible() || currentPos < 0){
			return false;
		}

		var val = $('#jq_tooltiplist li').eq(currentPos).attr('title');		
		$input.val(val);
		hideAutocomplete();		
		clearList();

		return true;
	}

}

$.icAutocompleter = function(){};

$.icAutocompleter.Cache = function(options) {

	var data = {};
	var length = 0;
	
	function matchSubset(s, sub) {
		if (!options.matchCase) 
			s = s.toLowerCase();
		var i = s.indexOf(sub);
		if (options.matchContains == "word"){
			i = s.toLowerCase().search("\\b" + sub.toLowerCase());
		}
		if (i == -1) return false;
		return i == 0 || options.matchContains;
	};
	
	function add(q, value) {
		if (length > options.cacheLength){
			flush();
		}
		if (!data[q]){ 
			length++;
		}
		data[q] = value;
	}
	
	function populate(){
		if( !options.data ) return false;
		// track the matches
		var stMatchSets = {},
			nullData = 0;

		// no url was specified, we need to adjust the cache length to make sure it fits the local data store
		if( !options.url ) options.cacheLength = 1;
		
		// track all options for minChars = 0
		stMatchSets[""] = [];
		
		// loop through the array and create a lookup structure
		for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
			var rawValue = options.data[i];
			// if rawValue is a string, make an array otherwise just reference the array
			rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
			
			var value = options.formatMatch(rawValue, i+1, options.data.length);
			if ( value === false )
				continue;
				
			var firstChar = value.charAt(0).toLowerCase();
			// if no lookup array for this character exists, look it up now
			if( !stMatchSets[firstChar] ) 
				stMatchSets[firstChar] = [];

			// if the match is a string
			var row = {
				value: value,
				data: rawValue,
				result: options.formatResult && options.formatResult(rawValue) || value
			};
			
			// push the current match into the set list
			stMatchSets[firstChar].push(row);

			// keep track of minChars zero items
			if ( nullData++ < options.max ) {
				stMatchSets[""].push(row);
			}
		};

		// add the data items to the cache
		$.each(stMatchSets, function(i, value) {
			// increase the cache size
			options.cacheLength++;
			// add to the cache
			add(i, value);
		});
	}
	
	// populate any existing data
	setTimeout(populate, 25);
	
	function flush(){
		data = {};
		length = 0;
	}
	
	return {
		flush: flush,
		add: add,
		populate: populate,
		load: function(q) {
			if (!options.cacheLength || !length)
				return null;
			/* 
			 * if dealing w/local data and matchContains than we must make sure
			 * to loop through all the data collections looking for matches
			 */
			if( !options.url && options.matchContains ){
				// track all matches
				var csub = [];
				// loop through all the data grids for matches
				for( var k in data ){
					// don't search through the stMatchSets[""] (minChars: 0) cache
					// this prevents duplicates
					if( k.length > 0 ){
						var c = data[k];
						$.each(c, function(i, x) {
							// if we've got a match, add it to the array
							if (matchSubset(x.value, q)) {
								csub.push(x);
							}
						});
					}
				}				
				return csub;
			} else 
			// if the exact item exists, use it
			if (data[q]){
				return data[q];
			} else
			if (options.matchSubset) {
				for (var i = q.length - 1; i >= options.minChars; i--) {
					var c = data[q.substr(0, i)];
					if (c) {
						var csub = [];
						$.each(c, function(i, x) {
							if (matchSubset(x.value, q)) {
								csub[csub.length] = x;
							}
						});
						return csub;
					}
				}
			}
			return null;
		}
	};
};


