/** 
 * Author and Version Information {{{
 * author: Antonio Ramirez http://webeaters.blogspot.com
 *
 * class: AutoComplete for Prototype 1.6.0
 *
 * version: 1.2.1 - 2007-11-11 
 * 		(based on AutoSuggest 2.1.3 - 2007-07-19)
 * version: 1.3.0 - 2008-01-03 by Andrew Nicols <andrew@nicols.co.uk>
 *  - Fixed incorrect title-casing - CSS is Case Sensitive!!!
 *  - Adjusted the way in which the Notifier images are loaded.
 *  - Changed json code to pass all json variables back instead of just id, value and name
 *  - Fixed 'GMAIL' code such that if valueSep is undefined, it is ignored
 *  - Changed the default for valueSep to null
 *  - Fixed the resetTimeout function
 *
 * REFERENCES AND THANKS 
 * this class is based on the work in AutoSuggest.js of
 * Timothy Groves - http://www.brandspankingnew.net
 * and adapted for use with prototype 1.6.0
 *
 * UPDATED by R��da HADJOUTI
 * GMAIL like AutoComplete (semicolon separator) Update
 *
 * UPDATED by Clarence Risher
 * OSCommerce specific product search modifications
 *
 *UPDATED by Douglas Early
 *CreateList now accepts additional AJAX inputs
 *.mfr - will accept the Manufacturer of a product and places it in the list in <small> tags like so - <small>by mfr</small>
 *.pic - takes a picture URI and combined with the baseURL paramter (which must be initialized when the class is instantiated) will insert a picture into the list before the text
 }}}*/

var AutoComplete = Class.create();

AutoComplete.prototype = { // {{{
  Version: '1.3.0',
  REQUIRED_PROTOTYPE: '1.6.0',

  initialize: function (id, param) { // {{{
  	// check whether we have the appropiate javascript libraries
  	this.PROTOTYPE_CHECK();
	
    // Get the field we're watching.
    // It needs to be a valid field so throw an error if it's not valid or can't be found.
    this.fld = $(id);
    if (!this.fld)
    {
      throw("AutoComplete requires a field id to initialize");
    }
	
    // Init variables
    this.sInp 	= ""; // input value 
    this.nInpC 	= 0;	// input value length
    this.aSug 	= []; // suggestions array 
    this.iHigh 	= 0;	// level of list selection 
	
  // Parameter Handling {{{
	// Set the use specified options
	this.options = param ? param : {};
	// These are the default settings {{{
  var k, def = {
    valueSep:null,
    minchars:1,
    meth:"get",
    varname:"input",
    className:"autocomplete",
    useTimeout: true,//Pass a flag to specify whether the autosuggest bubble will timeout or not
    timeout:3000,
    delay:500,
    offsety:-5,
    shownoresults: true,
    noresults: "No results were found.",
    maxheight: 250,
    cache: true,
    maxentries: 25,
    onAjaxError:null,
    setWidth: false,
    minWidth: 100,
    maxWidth: 200,
    useNotifier: true,
    blurHandler: function() { }//Pass this for additional onblur handling to be added to the autocomplete field
  };
  //}}}
  // Overlay any values which weren't user specified.
	for (k in def) 
	{
		if (typeof(this.options[k]) != typeof(def[k]))
			this.options[k] = def[k];
	}
  // End of Parameter Handling }}}

  // Not everyone wants to use the Notifier. Give them the option	
	if (this.options.useNotifier)
    {
        this.fld.addClassName('ac_field');
    }

    if (!this.options.useTimeout) {
        this.options.timeout = 1;
    }

	// set keyup handler for field
	// and prevent AutoComplete from client
	var p = this;
	
	// NOTE: not using addEventListener because UpArrow fired twice in Safari
	this.fld.onkeypress 	= function(ev){return p.onKeyPress(ev);};
	this.fld.onkeyup      = function(ev){return p.onKeyUp(ev);};
	this.fld.onkeydown      = function(ev){return p.onKeyDown(ev);};
  // ARN-DEBUG Chances are we want to reset the timeout when they lose focus, at least that's what I prefer
  //Set additonal onblur Handling for the autocomplete field; when needed
    if(this.options.mobile) {
        this.fld.onclick = function (ev) { ev.stopPropagation(); };
        document.body.onclick = function(ev) { p.clearSuggestions(); };
    } else {
        this.fld.onblur			  = function(ev){
                                                  if(p.options.blurHandler) p.options.blurHandler();
                                                  p.resetTimeout();
                                                  return true;
                                                };
    }

  // ARN-DEBUG Not sure what this is about!
	this.fld.setAttribute("AutoComplete","off");

  }, //}}}

  convertVersionString: function (versionString){ // {{{
      var r = versionString.split('.');
      return parseInt(r[0])*100000 + parseInt(r[1])*1000 + parseInt(r[2]);
  }, // }}}

  PROTOTYPE_CHECK: function() { // {{{
    if((typeof Prototype=='undefined') || 
       (typeof Element == 'undefined') || 
       (typeof Element.Methods=='undefined') ||
       (this.convertVersionString(Prototype.Version) < 
        this.convertVersionString(this.REQUIRED_PROTOTYPE)))
       throw("AutoComplete requires the Prototype JavaScript framework >= " +
        this.REQUIRED_PROTOTYPE);
  }, // }}}

  
  // set responses to keypress events in the field
  // this allows the user to use the arrow keys to scroll through the results
  // ESCAPE clears the list
  // RETURN sets the current highlighted value
  // UP/DOWN move around the list

  onKeyPress: function (e) { // {{{
  	if (!e) e = window.event;
  	var key	= e.keyCode || e.wich;

    switch(key)
    {
      case Event.KEY_RETURN:
        if(this.iHigh != 0) {
            this.fld.focus();

            if(this.aSug[this.iHigh - 1]) {
                sbm.submitHandler(this.aSug[this.iHigh - 1].name,this.aSug[this.iHigh - 1].type,this.aSug[this.iHigh - 1].id);
            }
            Event.stop(e);
        }
        this.clearSuggestions();
        break;
      case 40:
          if(!$(this.acID)) this.getSuggestions(this.fld.value,key);
        break;
      case Event.KEY_TAB:
        //this.clearSuggestions();
        //this.setHighlightedValue();
        //Event.stop(e);
        break;
      case Event.KEY_ESC:
        this.clearSuggestions();
        break;
    }
    return true;
  }, //}}}

  onKeyUp: function (e) { // {{{
  	if (!e) e = window.event;
  	
  	var key = e.keyCode || e.wich;
  	
  	if (key == Event.KEY_UP || key == Event.KEY_DOWN)
  	{
  		this.changeHighlight(key);
  		Event.stop(e);
  	} else if(key == Event.KEY_RETURN) {
        //stub - does nothing right now
    } else {
        this.getSuggestions(this.fld.value);
    }
  	
	return true;
  }, //}}}

  onKeyDown: function (e) { // {{{
  	if (!e) e = window.event;
  	
  	var key = e.keyCode || e.wich;
  	
	//IE fires onKeyDown in this case but not onKeyPress or onKeyUp
    if(key == Event.KEY_TAB)
	{
        if(this.iHigh != 0) {

            if(this.aSug[this.iHigh - 1]) {
                sbm.submitHandler(this.aSug[this.iHigh - 1].name,this.aSug[this.iHigh - 1].type,this.aSug[this.iHigh - 1].id);
            }
            
        }

	}
  	
	return true;
  }, //}}}

  getSuggestions: function(val,key) { // {{{
  	// input the same? do nothing
  	if(val==this.sInp && ($(this.acID) || !key)) return false;

  	this.sInp = val;
  	
  	// input length is less than the min required to trigger a request
  	// do nothing
  	if (val.length < this.options.minchars)
  	{
  		this.aSug 	= [];
  		this.nInpC	= val.length; 
  		return false;
  	}

  	// Here we will detect if there is a comma and the splitted value has a value to check
  	// comma stars a new search and val is converted to the new value after the comma
  	var ol	= this.nInpC; // old length
  	this.nInpC	= val.length ? val.length : 0;
  	
  	// if caching enabled, and we didn't receive the maxentries value
    // and user is typing (ie. length of input is increasing)
  	// filter results out of suggestions from last request
  	var l = this.aSug.length;
  	if( this.options.cache && ( this.nInpC > ol ) && l && ( l < this.options.maxentries ) )
  	{
  		var arr = new Array();
  		for (var i=0;i<l;i++) {
  			if (this.aSug[i].value.toLowerCase().indexOf(val.toLowerCase()) != -1)
        {
  				arr.push(this.aSug[i]);
        }
  		}
  		this.aSug = arr;
  		
  		// recreate the list
  		this.createList(this.aSug);
  	} else {
  		// do new request
  		var p = this;
  		clearTimeout(this.ajID); // ajax id timer
  		this.ajID = setTimeout( function () {p.doAjaxRequest(p.sInp)}, this.options.delay);
  	}
    document.helper = this;
  	return false;
  }, // }}}

  getLastInput : function(str) { // {{{
  	var ret = str;
  	if (undefined != this.options.valueSep) {
  		var idx = ret.lastIndexOf(this.options.valueSep);
      ret = idx == -1 ? ret : ret.substring(idx + 1, ret.length);
  	}
  	
  	return ret;
  }, // }}}

  doAjaxRequest: function (input) { // {{{
  	// we have to check here if there is a new splitted value (, or ;)
  	// always check against the last part of the comma and then check
  	// saved input is still the value of the field

  	// Gmail like : get only the last user's input
  	this.sInp = this.getLastInput(this.sInp);
 	
  	// create ajax request
  	var p = this;
    if( this.options.useNotifier )
    {
      this.fld.removeClassName('ac_field');
	    this.fld.addClassName('ac_field_busy');
    }
  	
  	// make new ajax request
    var request = xajax.request({xjxfun: 'search_autocomplete'},
                                {parameters: [input],
                                 onSuccess: function (req) {
                                                               if( p.options.useNotifier ) {
                                                                   p.fld.removeClassName('ac_field_busy');
                                                                   p.fld.addClassName('ac_field');
                                                               }
                                                               p.setSuggestions(req);
                                                           }
                                });
  }, // }}}

  setSuggestions: function (req) { // {{{
	  // if field input no longer matches what was passed to the request
	  // don't show the suggestions
	
    this.aSug = [];

    req = req.request;

    //Ghetto-fabulous conversion to JSON format
	var x = req.responseXML.getElementsByTagName('xjxrv')[0].childNodes;
    resultText = x[0].nodeValue;
    resultText = resultText.substring(1,resultText.length)

    if(this.options.json) 
    { // response in json format?
      var jsondata = eval('(' + resultText + ')');
      this.aSug = jsondata.results;
    } else {
      // response in xml format?
      var results = req.responseXML.getElementsByTagName('results')[0].childNodes;
    
      for(var i=0;i<results.length;i++)
      {
        if(results[i].hasChildNodes())
          this.aSug.push(  {'id':results[i].getAttribute('id'), 'value':results[i].childNodes[0].nodeValue, 'info':results[i].getAttribute('info')} );
      }
    }
    this.acID = 'ac_'+this.fld.id;
    this.createList(this.aSug);
  }, // }}}

  createDOMElement: function ( type, attr, cont, html ) { // {{{
    var ne = document.createElement( type );
	
    if (!ne)
      return 0;
      
    for (var a in attr)
      ne[a] = attr[a];
    
    var t = typeof(cont);
    
    if (t == "string" && !html)
      ne.appendChild( document.createTextNode(cont) );
    else if (t == "string" && html)
      ne.innerHTML = cont;
    else if (t == "object")
      ne.appendChild( cont );

    return ne;
  }, // }}}

  createList:	function(arr) { // {{{
    // clear list removal timeout
  	this.killTimeout();
  	
  	// if "suggest as you type" is disabled OR no results and showNoResults is false, do nothing
  	if (arr[0].disabled || (arr.length == 0 && !this.options.shownoresults)) return false;
  	
  	// create holding div
    var div;
    var new_list;
    if($(this.acID)) {
        div = $(this.acID);
        new_list = 0;
    } else {
        div	= this.createDOMElement('div', {id:this.acID, className:this.options.className});
        new_list = 1;
    }
  	
    // create and populate ul
    var ul	= this.createDOMElement('ul', {id:'ac_ul'});
    var p 	= this; // pointer that we will need later on
    // no results?
    if (arr.length == 0 && this.options.shownoresults)
    {
      var li = this.createDOMElement('li', {className: 'ac_warning'}, this.options.noresults );
      ul.appendChild(li);
    } else {
      // loop through arr of suggestions creating an LI element for each of them
      for (var i=0,l = arr.length; i<l; i++)
      {
        // format output with the input enclosed in a EM elementFromPoint
        // (as HTML not DOM)
        var mfr     = arr[i].mfr;
        if (!mfr) {
            mfr = ''
        } else {
            mfr = '<small> by '+mfr+'</small>';
        }

        var img = '';
        if(arr[i].pic) {
            var img_src = arr[i].pic;
            img = '<img src="'+img_src+'" style="display: inline; border: none; vertical-align: middle;" height="80px" width="60px" />';
        } else {
            img = '&nbsp;';
        }

        var type = '';
        var supertype = (arr[i].supertype) ? arr[i].supertype : '';
        if(arr[i].type) {
            type = '<small>'+arr[i].type+supertype+'</small>';
        }

        var output = arr[i].name+'<br />'+type;

        var inner_div_1 = this.createDOMElement('div',{},output,true);
        inner_div_1.style.width = '50%';
        inner_div_1.style.cssFloat = 'left';
        inner_div_1.style.display = 'inline';

        var inner_div_2 = this.createDOMElement('div',{},img,true);
        inner_div_2.style.width = '48%';
        inner_div_2.style.textAlign = 'right';
        inner_div_2.style.cssFloat = 'left';
        inner_div_2.style.display = 'inline';

        //earmarked for removal
        var inner_div_3 = this.createDOMElement('div',{},type,true);
        inner_div_3.style.width = '14%';
        inner_div_3.style.textAlign = 'right';
        inner_div_3.style.cssFloat = 'left';
        inner_div_3.style.display = 'inline';

        var outer_div = this.createDOMElement('div',{className:'search_ac_item'});
        outer_div.style.width = '100%';
        outer_div.style.padding = '6px 0 6px 0';
        outer_div.className = 'clearfix';

        outer_div.appendChild(inner_div_1);
        outer_div.appendChild(inner_div_2);

        var a 	= this.createDOMElement('a',{href:'#', className: 'ac_link',itemName: arr[i].name, itemType: arr[i].type, itemID: arr[i].id});

        a.appendChild(outer_div); // add the object span into the link
			
        a.name = i+1;
        
        a.onclick 		= function () { // {{{
          sbm.submitHandler(this.itemName,this.itemType,this.itemID);
        }; // }}}
        a.onmouseover	= function () { // {{{
          p.setHighlight(this.name); 
        }; // }}} 
			
        var li = this.createDOMElement('li', {}, a); // add the link element to a li element
        
        // finally add the newly created li element to the ul element 
        ul.appendChild(li);
      }
    }

    if(new_list) {
        div.appendChild(ul); // add the newly created list to the div element

        // get position of target textfield
        // position holding div below it
        // set width of holding div to width of field
        // if

        var pos         = this.fld.cumulativeOffset();
        div.style.left 	= pos[0] + "px";
        div.style.top 	= pos[1] + this.fld.offsetHeight + "px";

        var w =
        (
          this.options.setWidth && this.fld.offsetWidth < this.options.minWidth
        )
        ? this.options.minWidth :
        (
          this.options.setWidth && this.fld.offsetWidth > this.options.maxWidth
        )
        ? this.options.maxWidth :
        this.fld.offsetWidth;
        w = w - 2;

        div.style.width 	= w + "px";

        // set mouseover functions for div
        // when mouse pointer leaves div, set a timeout to remove the list after an interval
        // when mouse enters div, kill the timeout so the list won't be removed
        //If timeout is toggled off; the autosuggest bubble will be cleared when focus is lost
        if(this.options.useTimeout) {
            div.onmouseover 	= function(){p.killTimeout()};
            div.onmouseout 		= function(){p.resetTimeout()};
        }
        div.onclick     = function(e) { e.stopPropagation(); };
        
        // add DIV to document
        document.getElementsByTagName("body")[0].appendChild(div);
    } else {
        if ( div.hasChildNodes() ) {
            while ( div.childNodes.length >= 1 ) {
                div.removeChild( div.firstChild );
            }
        }
        div.appendChild(ul);
    }
    
    // highlight nothing until the user navigates with mouse or arrow keys
    this.iHigh = 0;
    this.setHighlight(0);
    
    // remove list after interval - as long useTimeout is true
    if(this.options.useTimeout) {
        this.toID	= setTimeout(
            function () {
                p.clearSuggestions()
            }, this.options.timeout
        );
    }
  }, // }}}

  changeHighlight:	function(key) { // {{{
  	var list = $("ac_ul");
    if (!list)
      return false;
	
    var n;

    n = (key == Event.KEY_DOWN || key == Event.KEY_TAB)? this.iHigh + 1 : this.iHigh - 1; // false assumed to be Event.KEY_UP
    
    n = (n > list.childNodes.length)? list.childNodes.length : ((n < 1)? 1 : n);	
    
    this.setHighlight(n);
  }, // }}}

  setHighlight:		function(n) { // {{{
  	var list = $('ac_ul');
  	
  	if (!list) return false;
  	
  	if (this.iHigh > 0) this.clearHighlight();
  	
  	this.iHigh = Number(n);
  	
  	if (n > 0) list.childNodes[this.iHigh-1].className = 'ac_highlight';
  	
  	this.killTimeout();
  }, // }}}

  clearHighlight:	function() { // {{{
  	var list = $('ac_ul');
  	
  	if(!list) return false;
  	
  	if(this.iHigh > 0)
  	{
  		list.childNodes[this.iHigh-1].className = '';
  		this.iHigh = 0;
  	}
  	
  }, // }}}

  setHighlightedValue:	function() { // {{{
    //dbg:  Dearly this function seems to be broken; works once and then kills AC functionality on the search field
  	if (this.iHigh)
  	{
  		// HERE WE NEED TO IMPLEMENT THE GMAIL LIKE SPLITTED VALUE
  		if (!this.aSug[this.iHigh - 1]) return;
  		
  		// Gmail like
      if (undefined != this.options.valueSep) {
        var str = this.getLastInput(this.fld.value);
        var idx = this.fld.value.lastIndexOf(str);
        str = this.aSug[ this.iHigh -1 ].name + this.options.valueSep;
        this.sInp = this.fld.value = idx == -1 ? str : this.fld.value.substring(0, idx) + str;
      } else {
        var str = this.getLastInput(this.fld.value);
        var idx = this.fld.value.lastIndexOf(str);
        str = this.aSug[ this.iHigh -1 ].name;
        this.sInp = this.fld.value = idx == -1 ? str : this.fld.value.substring(0, idx) + str;
      }
  		
  		// move cursor to end of input (safari)
  		this.fld.focus();
  		if(this.fld.selectionStart)
  			this.fld.setSelectionRange(this.sInp.length, this.sInp.length);
  			
  		this.clearSuggestions();
  		
  		// pass selected object to callback function, if exists
  		if (typeof this.options.callback == 'function')
  			this.options.callback(this.aSug[this.iHigh-1]); // the object has the properties we want, it will depend of
  	}
  }, // }}}

  killTimeout:	function() { // {{{
  	clearTimeout(this.toID);
  }, // }}}

  resetTimeout:	function() { // {{{
  	this.killTimeout();
  	var p = this;
  	this.toID = setTimeout(
      function () { 
        p.clearSuggestions();
      }, p.options.timeout
    );
    // ARN-DEBUG Added p.options.timeout back :|
  }, // }}}

  clearSuggestions:	function () { // {{{

    this.killTimeout();
    if ($(this.acID))
    {
        this.fadeOut(300,function () {
        $(this.acID).remove();
      } );
    }
  }, // }}}

  fadeOut:	function (milliseconds, callback) { // {{{
  	this._fadeFrom 	= 1;
  	this._fadeTo	= 0;
  	this._afterUpdateInternal = callback;
  	
  	this._fadeDuration	= milliseconds;
  	this._fadeInterval = 50;
  	this._fadeTime = 0;
  	var p = this;
  	this._fadeIntervalID = setInterval(
      function() {
        p._changeOpacity()
      }, this._fadeInterval
    );
  
  }, // }}}

  _changeOpacity: function() { // {{{
 
    if (!$(this.acID))
    {
  		this._fadeIntervalID=clearInterval(this._fadeIntervalID);
  		return;
  	} 
  	this._fadeTime += this._fadeInterval;
  	
  	var ieop = Math.round( (this._fadeFrom + ((this._fadeTo - this._fadeFrom) * (this._fadeTime/this._fadeDuration))) * 100)
  	var op = ieop / 100;
 
  	var el = $(this.acID);
  	if (el.filters) // internet explorer
  	{
      try {
        el.filters.item("DXImageTransform.Microsoft.Alpha").opacity = ieop;
      } catch (e) { 
        // If it is not set initially, the browser will throw an error.
        // This will set it if it is not set yet.
        el.style.filter = 'progid:DXImageTransform.Microsoft.Alpha(opacity='+ieop+')';
      }
    } else	{
      el.style.opacity = op;
    }
	
    if (this._fadeTime >= this._fadeDuration)
    {
      clearInterval( this._fadeIntervalID );
      if (typeof this._afterUpdateInternal == 'function')
        this._afterUpdateInternal();
    }

  } // }}}
 
} // }}}

// vim: set filetype=javascript foldmethod=marker foldlevel=5:

