var Suggest = {
	cache: new Object()	,           //Cache of results for from the server, a hash indexed by data source description, containing arrays
	cacheCurrentPage: new Hash(), //Cache of new options entered in boxes on the current page (ones not yet on the server)
	maxSuggestItems: 20,
	  
  updateSuggestions: function(e,select)
  {
	  //If there was a key command don't rebuild the suggest box (we would loose selection position)
	  if(this.handleSpecialKeys(e))
	    return;
	
	  el=e.element();
    if(select===true)
      el.select();
    source = new SuggestSource(el);
	  this.fetchData(el,source, this.updateSuggestionsWithData.bind(this,el));
  },

  //Save a new option used in a box on the current page to the local cach if it is not already in there or in the ajax cache
  updateCacheCurrentPage: function(e)
  {
	  el=e.element();
	  source = new SuggestSource(el);
	  sourceName=source.toString();
	  if(this.cacheCurrentPage.get(sourceName)==undefined)
		  this.cacheCurrentPage.set(sourceName, new Hash());
		
		//If the value is empty or we already have it then don't keep it in the current page cache
		if(
			el.value=='' ||
			this.cache[sourceName].indexOf(el.value)!=-1 || //Not in the ajax cache
      this.cacheCurrentPage.get(sourceName).find(function(v) { return v[1]==el.value && v[0]!=el.id; } )!=undefined //No OTHER field had this value
    )
	  {
		  this.cacheCurrentPage.get(sourceName).unset(el.id);
		  return;
		}
		this.cacheCurrentPage.get(sourceName).set(el.id,el.value);
  },

  updateDataCache: function(el)
  {
 	  this._removeSuggestBox(el);
    source = new SuggestSource(el);
	  sourceName=source.toString();  
	  if(this.cache[sourceName]!=undefined)
		  return;
	  this.fetchData(el,source, function() {}); //Fetchdata with a dummy postfetch callback
  },

  fetchData: function(el,source, dataReadyCallback) {
	  sourceName=source.toString();  
	  if(this.cache[sourceName]!=undefined) {
  	  this.updateSuggestionsWithData(el,this.cache[sourceName]);
		  return;
	  }
	  
	  new Ajax.Request('suggest-postoffice', {
		  method: 'post',
		  parameters: source.toPostData(),
		  onSuccess: function(el, source, dataReadyCallback, transport) {
        if(transport.responseText=='')
          return;
        var decodedJson = transport.responseText.evalJSON(true);
        this.fetchDataCallBack(el, source, dataReadyCallback, decodedJson);
		  }.bind(this,el, source, dataReadyCallback)
		});

	
  },

  fetchDataCallBack: function(el,source,dataReadyCallback,data) {
  	sourceName=source.toString();
	  this.cache[sourceName]=data;  
	  dataReadyCallback(data);
  },

  updateSuggestionsWithData: function(el,data)
  {
	  source = new SuggestSource(el);
	  sourceName=source.toString();

    ajaxSuggestData=this.filterData(el,data);

    //Merge in options collected in the current page
    currentPageSuggestData=this.cacheCurrentPage.get(sourceName);
    currentPageSuggestData=(currentPageSuggestData==undefined) ? Array() : this.filterData(el,currentPageSuggestData.values());
    suggestData=ajaxSuggestData.concat(currentPageSuggestData);
    suggestData.sort();

    if(suggestData.length==0) {
	    this._removeSuggestBox(el);
    }
    else {
	    suggestEl=this.prepareSuggestBox(el);
	    for(idx=0; idx<suggestData.length && idx<this.maxSuggestItems; idx++) {
		    this.addSuggestItem(el,suggestEl,suggestData[idx]);
	    }
    }

  },

  filterData: function(el,data) {
	  value=el.value;
		value=value.replace(/\\/g,'\\\\');
		value=value.replace(/\'/g,'\\\'');
		value=value.replace(/\"/g,'\\"');
		value=value.replace(/\0/g,'\\0');
	  regex=new RegExp('^'+value,'i');
	  return data.grep(regex);
  },

  getSuggestBox: function(el) {
	  suggestID=el.id+'_suggestions';
	  return $(suggestID);
	},
  

  prepareSuggestBox: function(el) {
	  suggestID=el.id+'_suggestions';
	  if($(suggestID)) {
	    suggestEl=$(suggestID);
			while(suggestEl.firstChild) { 
			  suggestEl.removeChild(suggestEl.firstChild);
			}
	  }
	  else {
		  suggestEl=new Element('ul', { 'class': 'suggestPulldown', id: suggestID });
		  el.up().insert(suggestEl);
	    suggestEl.clonePosition(el,{offsetLeft:0,offsetTop:el.getHeight(),setHeight:false,setWidth:false});
	  }
	  return suggestEl;
	},
	
  //Add a delay so that the on click of a suggestion is not in a race with the onblur of the text box
  removeSuggestBox: function(el)
  {
	  func=this._removeSuggestBox.bind(this,el);
    new PeriodicalExecuter(function(pe) { func(); pe.stop(); }, 0.2);
  },

  _removeSuggestBox: function(el)
  {
	  suggestID=el.id+'_suggestions';
	  if($(suggestID)) {
	    $(suggestID).remove();
	  }
	},
	
	addSuggestItem: function(el,box,item)
	{
		itemEl=new Element('li', {}).update(item);
		box.appendChild(itemEl);
		itemEl.observe('click', this.chooseSuggestion.bind(this,el,item) );
		itemEl.observe('mouseover', itemEl.addClassName.bind(itemEl,'hover') );
		itemEl.observe('mouseout', itemEl.removeClassName.bind(itemEl,'hover') );
		
		//box.setAttribute('size',box.options.length);
	},
	
	chooseSuggestion: function(el,item) {
		el.value=item;
		this.removeSuggestBox(el);
	},
	
	selectNextSuggestion: function(el) {
		suggestEl=this.getSuggestBox(el);
		//If empty do nothing
		if(!suggestEl || !suggestEl.firstDescendant())
		  return false;
		
    //Find Current
		currentItem=suggestEl.down('li.selected');
		if(currentItem) {
			currentItem.removeClassName('selected');
			next=currentItem.next('li');
			if(next===undefined)
			  next=suggestEl.firstDescendant();
			next.addClassName('selected');
		}
		else {
			suggestEl.firstDescendant().addClassName('selected');
		}
	},
	
	selectPrevSuggestion: function(el) {
		suggestEl=this.getSuggestBox(el);
		//If empty do nothing
		if(!suggestEl.firstDescendant())
		  return false;
		
    //Find Current
		currentItem=suggestEl.down('li.selected');
		if(currentItem) {
			currentItem.removeClassName('selected');
			prev=currentItem.previous('li');
			if(prev===undefined) {
				//Find last descendant
			  dec=suggestEl.immediateDescendants();
			  prev=dec[dec.length-1];
			}
			prev.addClassName('selected');
		}
		else {
			//Find last descendant
		  dec=suggestEl.immediateDescendants();
		  dec[dec.length-1].addClassName('selected');
		}		
	},

  chooseSelectedSuggestion: function(el) {
		suggestEl=this.getSuggestBox(el);
		if(!suggestEl)
		  return;
    //Find Current
		currentItem=suggestEl.down('li.selected');
		if(!currentItem) {
		  return;
    }
    this.chooseSuggestion(el,currentItem.innerHTML);
  },
	
	handleSpecialKeys: function(e)
	{
		el=e.element();
		if(e.keyCode==Event.KEY_ESC) {
	    this.removeSuggestBox(el);
	  } else if(e.keyCode==Event.KEY_DOWN) {
	    this.selectNextSuggestion(el);
	  } else if(e.keyCode==Event.KEY_UP) {
	    this.selectPrevSuggestion(el);
	  } else if(e.keyCode==Event.KEY_RETURN || e.keyCode==Event.KEY_TAB) {
		  this.chooseSelectedSuggestion(el);
		}
		else
		  return false;
		return true;
	},
	
	blockSpecialKeyEvent: function(e)
	{
    if(e.keyCode==Event.KEY_RETURN) {
	    this.chooseSelectedSuggestion(el);
      e.returnValue=false;
      e.cancel = true;
    }
	},
	
	setupWatchingDataParents: function(el) {
		params=el.getAttribute('suggestparams');
		params=params.split(',').reverse();
		idReg=/\<(.*)\>/
		for(i=0; i<params.length; i++) {
		  var idbits=idReg.exec(params[i]);
			if(idbits) {
				dataParentEl=$(idbits[1]);
		        if(!dataParentEl)
		          continue
		        dataParentEl.observe('change', this.updateDataCache.bind(this,el) );
		        if(el.getAttribute('updateOnLoad')=='1')
		          this.updateDataCache(el); //And grab the current data
		        return; //Only watch one parameter (the last one in the list with an ID) to avoid ajax collissions
			}
    }
	},
	
	setupEvents: function(rootEl) {
		//Not supplied, do the whole document (ie at onload)
		if(rootEl==undefined)
		  elements=$$('.suggestText')
		else { //If supplied add events to a new portion of the document
			 //I ran into a case where in IE7 rootEl from popup.js was not extended with Element but in FF and safari it was
		  elements=Element.select(rootEl,'.suggestText');
    }
		elements.each(function(el) {
		  el.observe('keydown', function(evnt) { Suggest.blockSpecialKeyEvent(evnt,false) } );
		  el.observe('keyup', function(evnt) { Suggest.updateSuggestions(evnt,false) } );
		  el.observe('focus', function(evnt) { Suggest.updateSuggestions(evnt,true) } );
		  el.observe('blur', function(evnt) { Suggest.removeSuggestBox(evnt.element()); Suggest.updateCacheCurrentPage(evnt); } );
		  Suggest.setupWatchingDataParents(el);
		});
		
	}
	
}


// Suggest data Location
function SuggestSource(el) {
	if(el!=undefined) {
		this.getSourceFromElement(el);
	}
}
SuggestSource.prototype.className;
SuggestSource.prototype.methodName;
SuggestSource.prototype.params;
SuggestSource.prototype.getSourceFromElement = function(el) {
		this.className=el.getAttribute('suggestclass');
		this.methodName=el.getAttribute('suggestmethod');
		this.params=el.getAttribute('suggestparams');
		this.params=this.params.split(',');
		idReg=/\<(.*)\>/
		for(i=0; i<this.params.length; i++) {
		  var idbits=idReg.exec(this.params[i]);
			if(idbits) {
				el=$(idbits[1]);
				this.params[i]=el ? el.getValue() : false;
			}
    }
    return true;
}
SuggestSource.prototype.toString = function() {
    return this.className+'::'+this.methodName+'('+this.params.join(',')+')';
}
SuggestSource.prototype.toPostData = function() {
  var data = {};
  data['className'] = this['className'];
  data['methodName'] = this['methodName'];
  data['params[]'] = this['params'];
  return data;
}

Event.observe(window, 'load', function() {
	Suggest.setupEvents();
});
