/**
 * Auto-complete for text inputs.
 * Requires Prototype 1.5.0
 * @author Mike Daly, BizShark
 */

/******************************** AutoComplete ********************************/
AutoComplete = Class.create();
AutoComplete.Remote = Class.create();

AutoComplete.START  = 1;    // only match text at the very beginning of the data
AutoComplete.GLOBAL = 2;    // match text anywhere in the data
AutoComplete.WORDS  = 3;    // match text at the beginning of individual words in the data

AutoComplete.DefaultOptions = {
  caseSensitive: false,           // whether matching is case-sensitive
  matching: AutoComplete.START,   // how to match the text
  showIfEmpty: false,             // whether show all options when there is no text entered
  onSelect: null                  // callback when a value is chosen
};

AutoComplete.Remote.DefaultOptions = {
  minLength: 1                    // minimum number of characters entered before sending a remote request
};

AutoComplete.prototype = {
  initialize: function(element, dataArray, displayArray, options) {
    element = $(element);
    if (!element || !element.parentNode || (element.tagName || '').toLowerCase() != 'input' || (element.getAttribute('type') || '').toLowerCase() != 'text') return;
    
    this.options = Object.extend(Object.extend({}, AutoComplete.DefaultOptions), options || {});
    if (typeof this.options.onSelect != 'function') this.options.onSelect = null;
    
    this.dataArray = dataArray || [];
    this.displayArray = (displayArray && displayArray.length == this.dataArray.length) ? displayArray : this.dataArray;
    this.displayedIndexes = [];
    this.displayedIndex = -1;
    this.element = element;
    this.visible = false;
    
    this.container = document.createElement('div');
    this.container.style.display = 'none';
    this.container.className = 'autocomplete';
    this.container.appendChild(document.createElement('div'));
    
    if (AutoComplete.documentLoaded) document.body.appendChild(this.container);
    else Event.observe(window, 'load', function(){ document.body.appendChild(this.container); }.bind(this));
    
    this.valueDivOnclick = function(e) {
      var elem = Event.element(window.event || e);
      while (elem && !elem.className.match('autocomplete_value'))
        elem = elem.parentNode;
      
      this.displayedIndex = elem.displayedIndex;
      this.valueChosen();
      this.element.focus();
      return false;
    }.bind(this);
    
    Event.observe(this.element, 'keypress', this.onKeyPress.bindAsEventListener(this), false);
    Event.observe(this.element, 'keyup', this.onKeyUp.bindAsEventListener(this), false);
    Event.observe(document, 'click', this.hide.bindAsEventListener(this), false);
    AutoCompleteManager.register(this);
  },
  
  //***** public
  
  show: function() {
    if (!this.visible) this.displayedIndex = -1;
    this.updateValue();
    
    if (this.displayedIndexes.length > 0) {
      this.initPosition();
      this.visible = true;
      Element.show(this.container);
    } else if (this.visible) {
      this.hide();
    }
  },
  
  hide: function() {
    this.visible = false;
    Element.hide(this.container);
  },
  
  updateValue: function() {
    this.valueUpdated = true;
    var compareVal = this.options.caseSensitive ? this.element.value : this.element.value.toLowerCase();
    var prevIndex = this.dataArrayIndex();
    this.displayedIndexes = [];
    this.selectedElem = null;
    
    var valuesDiv = document.createElement('div');
    valuesDiv.className = 'autocomplete_values';
    
    if (compareVal.length > 0 || this.options.showIfEmpty) {
      for (var i = 0; i < this.displayArray.length; i++) {
        var arrayVal = this.displayArray[i] ? this.displayArray[i].toString() : '';
        var compareArrayVal = this.options.caseSensitive ? arrayVal : arrayVal.toLowerCase();
        var index = compareArrayVal.indexOf(compareVal);
        var isMatched = false;
        
        switch (this.options.matching) {
          case AutoComplete.START:
            isMatched = (index == 0); break;
          case AutoComplete.GLOBAL:
            isMatched = (index != -1); break;
          case AutoComplete.WORDS:
            if (index == 0) {
              isMatched = true;
              break;
            }
            
            while (index != -1) {
              var c = arrayVal.charAt(index - 1);
              isMatched = (c == ' ' || c == '(');
              if (isMatched) break;
              index = compareArrayVal.indexOf(compareVal, index + 1);
            }
            
            break;
        }
      
        if (isMatched) {
          var valueDiv = document.createElement('a');
          valueDiv.href = '#';
          valueDiv.onclick = this.valueDivOnclick;
          valueDiv.displayedIndex = this.displayedIndexes.length;
          valueDiv.className = (i == prevIndex) ? 'autocomplete_value autocomplete_selected' : 'autocomplete_value';
          valueDiv.innerHTML = arrayVal.substring(0, index) + '<span class="autocomplete_highlight">' +
                               arrayVal.substring(index, index + compareVal.length) + '</span>' +
                               arrayVal.substring(index + compareVal.length);
          
          valuesDiv.appendChild(valueDiv);
          if (i == prevIndex) {
            this.selectedElem = valueDiv;
            this.displayedIndex = this.displayedIndexes.length;
          }
          this.displayedIndexes.push(i);
        }
      }
    }
    
    this.container.replaceChild(valuesDiv, this.container.firstChild);
    if (!this.selectedElem) this.displayedIndex = -1;
  },
  
  selectPrevious: function() {
    if (this.displayedIndex < 1) return;
    this.select(-1);
  },
  
  selectNext: function() {
    if (this.displayedIndex >= this.displayedIndexes.length - 1) return;
    this.select(1);
  },
  
  dataArrayIndex: function() {
    return (this.displayedIndex == -1 ? -1 : this.displayedIndexes[this.displayedIndex]);
  },
  
  //***** private
  
  onKeyPress: function(event) {
    if (event.altKey || event.ctrlKey || event.metaKey) return;
    
    switch (event.keyCode) {
      case Event.KEY_TAB:
      case Event.KEY_RETURN:
        if (this.visible) {
          this.valueChosen();
          if (this.displayedIndex > -1) {
            Event.stop(event);
            return false;
          }
        }
        break;
      case Event.KEY_ESC:
        if (this.visible) {
          this.hide();
          Event.stop(event);
        }
        break;
      case Event.KEY_UP:
        this.visible ? this.selectPrevious() : this.show();
        Event.stop(event);
        break;
      case Event.KEY_DOWN:
        this.visible ? this.selectNext() : this.show();
        Event.stop(event);
        break;
      case Event.KEY_LEFT:
      case Event.KEY_RIGHT:
      case Event.KEY_HOME:
      case Event.KEY_END:
      case Event.KEY_PAGEUP:
      case Event.KEY_PAGEDOWN:
        break;
      default:
        this.allowUpdateValue = true;
    }
  },
  
  onKeyUp: function() {
    if (this.allowUpdateValue) {
      this.allowUpdateValue = false;
      this.show();
    }
  },
  
  select: function(incr) {
    this.displayedIndex += incr;
    
    if (this.selectedElem) this.selectedElem.className = 'autocomplete_value';
    this.selectedElem = this.container.firstChild.childNodes[this.displayedIndex];
    this.selectedElem.className = 'autocomplete_value autocomplete_selected';
    
    var scrollTop = this.container.firstChild.scrollTop;
    var scrollBottom = scrollTop + this.container.firstChild.offsetHeight;
    var elemTop = this.selectedElem.offsetTop;
    var elemBottom = elemTop + this.selectedElem.offsetHeight;
    
    if (elemTop < scrollTop) this.container.firstChild.scrollTop = elemTop;
    else if (elemBottom > scrollBottom) this.container.firstChild.scrollTop = elemBottom - this.container.firstChild.offsetHeight;
  },
  
  valueChosen: function() {
    this.hide();
    
    if (this.displayedIndex > -1) {
      this.element.value = (this.dataArray[this.dataArrayIndex()] || '').toString();
      if (this.options.onSelect) this.options.onSelect(this.element.value);
    }
  },
  
  initPosition: function() {
    var x = this.getX(this.element);
    var y = this.getY(this.element);
    
    if (this.positionInitialized && this.x == x && this.y == y) return;
    this.positionInitialized = true;
    
    this.x = x;
    this.y = y;
    this.w = this.getW(this.element);
    
    this.container.style.position = 'absolute';
    this.container.style.left  = this.x + 'px';
    this.container.style.top   = this.y + 'px';
    this.container.style.width = this.w + 'px';
  },
  
  getX:   function(elem) { return this.left(elem); },
  getY:   function(elem) { return this.top(elem) + this.height(elem); },
  getW:   function(elem) { return this.width(elem); },
  left:   function(elem) { return elem ? ((Prototype.Browser.IE ? elem.clientLeft + elem.offsetLeft : elem.offsetLeft) + this.left(elem.offsetParent)) : 0; },
  top:    function(elem) { return elem ? ((Prototype.Browser.IE ? elem.clientTop  + elem.offsetTop  : elem.offsetTop)  + this.top(elem.offsetParent))  : 0; },
  width:  function(elem) { return elem.offsetWidth; },
  height: function(elem) { return elem.offsetHeight; }
};

/******************************** AutoComplete.Remote ********************************/
Object.extend(Object.extend(AutoComplete.Remote.prototype, AutoComplete.prototype), {
  initialize: function(element, url, options) {
    options = Object.extend(Object.extend({}, AutoComplete.DefaultOptions), options || {});
    AutoComplete.prototype.initialize.call(this, element, [], [], options);
    this.url = url;
    this.tries = 0;
    this.complete = true;
  },
  
  show: function() {
    if (this.element.value.strip().length >= this.options.minLength) {
      this.updateValue();
    } else if (this.visible) {
      this.hide();
    }
  },
  
  updateValue: function(tries) {
    var t = tries || ++this.tries;
    if (this.complete) {
      this.complete = false;
    
      if (t == this.tries) {
        var q = this.element.value;
        new Ajax.Request(this.url, {
          asynchronous:true,
          evalScripts:false,
          parameters:('q=' + q.strip()),
          onComplete: function(r) { this.onUpdate(r, q, t); }.bind(this)
        });
      } else {
        this.complete = true;
      }
    } else {
      setTimeout(function() { this.updateValue(t); }.bind(this), 100);
    }
  },
  
  onUpdate: function(req, query, tries) {
    var validResponse = this.handleResponse(req, query);
    
    if (tries == this.tries) {
      this.displayedIndexes = [];
      this.selectedElem = null;
      
      var valuesDiv = document.createElement('div');
      valuesDiv.className = 'autocomplete_values';
      
      for (var i = 0; i < this.dataArray.length; i++) {
        var valueDiv = document.createElement('a');
        valueDiv.href = '#';
        valueDiv.displayedIndex = this.displayedIndexes.length;
        valueDiv.className = 'autocomplete_value';
        valueDiv.innerHTML = this.displayArray[i];
        valueDiv.onclick = this.valueDivOnclick;
        
        valuesDiv.appendChild(valueDiv);
        this.displayedIndexes.push(i);
      }
      
      if (tries == this.tries) {
        this.container.replaceChild(valuesDiv, this.container.firstChild);
        this.displayedIndex = -1;
        
        if (this.displayedIndexes.length > 0) {
          this.initPosition();
          this.visible = true;
          Element.show(this.container);
        } else if (this.visible) {
          this.hide();
        }
      }
    }
    
    this.complete = true;
  },
  
  handleResponse: function(request, query) {
    var values = null;
    try { values = eval(request.responseText); } catch (e) {}
    if (!values || !(values instanceof Array)) return false;
    
    this.dataArray = values;
    this.displayArray = values;
    
    return true;
  }
});

/******************************** AutoCompleteManager ********************************/
AutoCompleteManager = {
  nextId: 1,
  autoCompletes: {},
  
  register: function(autoComplete) {
    var id = 'autocomplete_' + (autoComplete.element.id || this.nextId++);
    while (this.autoCompletes[id]) id = 'autocomplete_' + this.nextId++;
    
    autoComplete.id = id;
    autoComplete.container.id = id;
    this.autoCompletes[id] = autoComplete;
  }
};

Event.observe(window, 'load', function(){ AutoComplete.documentLoaded = true; });