/**
 * Autocomplete input for Strike
 * 
 * @param note: update system is based on RealTime
 * 
 * @param urlOrData: can be : 
 *                  - a string url returning an array of results in json format (ajax GET request passing two params : search (string) and limit (int) )
 *                  - an array of results
 *        results are : Array of {id, value, visibility*} 
 *        * visibility (boolean) is optional, you can force the visibility / non visibility of a result
 * 
 * @param options object :
 *  .onSelect : function called when item is selected
 *  .onFocus  : function called when autocomplete take the focus
 *  .onBlur   : function called when autocomplete release the focus
 *  .scroll : (boolean) use an iScroll for results : note that your .results node must have a parent restricting a max-height
 *  .minInterval : (int) min time interval between each update (ms)
 *  .maxInterval : (int) max time interval, used when user write something, and minInterval is ignored
 *  .minTextLength : (int) if text length < minTextLength, considering no text changed except the empty update
 *  .strategy : The autocomplete match strategy. It can be :
 *                  * 'contains' if the string contains the search
 *                  * 'begin' if the string begin with the search
 *                  * 'tags' if the string begin with the first word in the search and contains all others search words
 *                  * ... (todo)
 */

var Autocomplete = function(node, urlOrData, o) {
  var self = this;
  
  // Required args
  self.container = node;
  if(typeof(url)=='string')
    self.ajaxUrl = urlOrData;
  else
    self.data = urlOrData || [];
  
  // Options
  self.scroll = !o['scroll'] ? false : true;
  self.limit = o['limit'] ? o.limit : 10;
  self.strategy = o['strategy'];
  if(!self.strategy || (self.strategy!='contains' && self.strategy!='begin' && self.strategy!='tags'))
    self.strategy = 'tags'; // Default strategy fallback
  self.onSelect = o['onSelect'] ? o.onSelect : function(){};
  self.onFocus = o['onFocus'] ? o.onFocus : function(){};
  self.onBlur = o['onBlur'] ? o.onBlur : function(){};
  self.templateLi = o['templateLi'] || function(o, search) {
    return '<li value="'+(o['id']||'')+'">'+self.highlight((o['value']||''), search)+'</li>';
  };
  
  // autocomplete datas / states
  self.selectedValue = null;
  self.selectedId = null;
  self.results = [];
  self.input = $('input', self.container)[0];
  self.resultsNode = $('ul.results', self.container)[0];
  self.resultsDisplayed = false;
  self.container.autocompleteInstance = self;
  
  // Utils
  var highlightReplaceForTag = function(value, tag) {
    return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + tag.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>"); // taken from jquery.autocomplete.js
  }
  self.highlight = function(value, term) {
    if(self.strategy == 'tags') {
      var tags = term.split(/\s+/);
      for(var t=0; t<tags.length; ++t)
        value = highlightReplaceForTag(value, tags[t]);
      return value;
    }
    else
      return highlightReplaceForTag(value, term);
  }
  
  // Called when input has changed and autocomplete need to be updated
  self.autocomplete = function() {
    var search = self.input.value;
    $.addClass(self.container, 'searching');
    if(self.ajaxUrl) {
      var query = self.ajaxUrl+"?search="+encodeURIComponent(search)+'&limit='+self.limit;
      $.ajax("GET", query, function(data){
        self.results = data
        self.displayAutocomplete(search)
      });
    }
    else {
      var res = []
      var i = 0;
      if(search) {
        var tags = search.toLowerCase().split(/\s+/);
        $.each(self.data, function(el) {
          if(i>self.limit) return;
          var visible = false;
          if(typeof(el.visibility)=='undefined') { // visibility is not set
            var value = el.value.toLowerCase();
            if(self.strategy == 'tags') {
              var match = value.indexOf(tags[0])==0;
              for(var i=1; i<tags.length && match; ++i)
                if(value.indexOf(tags[i])==-1)
                  match = false;
              if(match)
                visible = true;
            }
            else {
              var index = value.indexOf(search.toLowerCase());
              if(self.strategy=='contains' && index!=-1 || self.strategy=='begin' && index==0)
                visible = true;
            }
          }
          else {
            visible = el.visibility;
          }
          if(visible) {
            ++i;
            res.push(el);
          }
        });
      }
      self.results = res;
      self.displayAutocomplete(search);
    }
    return self;
  }
  
  // Display autocomplete results for a specific search
  self.displayAutocomplete = function(search) {
    $.removeClass(self.container, 'searching');
    var results = self.results;
    var ul = '';
    $.each(results, function(el) {
      ul += self.templateLi(el, search);
    });
    self.resultsNode.innerHTML = ul;
    self.resultsDisplayed = results.length>0;
    
    $.each($('li', self.resultsNode), function(li, i){
      $.on(li, 'click', function(e) {
        self.select(self.results[i]);
      });
    });
    return self;
  }
  
  // Select a specific element
  self.select = function(el) {
    self.resultsNode.innerHTML = '';
    self.selectedValue = self.input.value = !el ? null : el.value;
    self.container.value = self.selectedId = !el ? null : el.id;
    self.resultsDisplayed = false;
    self.onBlur();
    if(self.onSelect && !!el)
      self.onSelect(el);
    return self;
  };
  
  // Events handling
  $.on(self.input, 'focus', function(e){
    if(!self.resultsDisplayed) {
      self.input.value = '';
      self.onFocus(); // take the focus
    }
  });
  $.on(self.input, 'blur', function(e){
    if(!self.resultsDisplayed || !self.input.value) {
      self.input.value = self.selectedValue || '';
      self.onBlur(); // release the focus
    }
  });
  
  if(self.scroll && iScroll) {
    if(self.container.scroller)
      self.container.scroller.destroy();
    self.container.scroller = new iScroll(self.resultsNode, { desktopCompatibility: !Strike.onMobile } );
  }
  
  // Using RealTime to smartly manage the autocomplete update
  o.update = self.autocomplete;
  new RealTime(self.input, o); 
}


 /**
  * Update system based on https://github.com/adrenalinup/Tsunami/blob/master/project/public/javascripts/tsunami/tools/real-time.js
  * Manage input (or textarea) smart callback on change
  * @author Gaetan Renaudeau <pro@grenlibre.fr>, Namolovan Nicolae <n.i.c.o.l.a.e.namolovan@gmail.com>
  */
var RealTime = function(node, o) {
  var self = this;
  this.input = node;
  
  var getTime = function() {
    return new Date().getTime();
  }
  
  self.callback = o['update'] || function(){};
  self.textChange = o['textChange'] || function(){};
  self.timer = false;
  self.lastCall = getTime();
  self.lastSearch = self.input.value;
  self.minInterval = o['minInterval'] || 800; // min interval between each call to minimize requests
  self.maxInterval = o['maxInterval'] || 800; // max interval, to allow some updates
  self.minTextLength = o['minTextLength'] || 0;
  
  if (self.minInterval > self.maxInterval) self.maxInterval = self.minInterval + 100;
  
  // first couch
  this.onKeyup = function(e) {
    if (self.timer === false) {
        self.lastCall = getTime();
        self.timerStarted = getTime();
        self.ignoredKeys = 0;
        self.onInputChange();
    } 
    else {
      if ( /* maxInterval */ (getTime() - self.timerStarted) > self.maxInterval ) {
        clearTimeout(self.timer); // Don't need the timer anymore
        self.timer = false;
        self.lastCall = self.timerStarted; // Force to execute the update code
        self.onInputChange();
      }
      else {
        self.lastCall = getTime();
      }
    }
  }
  
  // second couch
  self.onInputChange = function() {
    var now = getTime();
    var newInputValue = self.input.value;
    var inputValue = self.lastSearch;
    if(/* text has changed */ inputValue!=newInputValue
    && /* enough letters to update */ (newInputValue.length==0 || newInputValue.length>=self.minTextLength) ) {
      self.textChange(inputValue);
      if(now-self.lastCall>self.minInterval) { /* min interval elapsed */
        /// UPDATE
        inputValue = newInputValue;
        self.lastSearch = inputValue;
        self.callback(inputValue, self.input);
        self.lastCall = getTime();
        self.timer = false;
      }
      else {
        self.timer = setTimeout(self.onInputChange, self.minInterval-(now-self.lastCall)+10);
      }
    }
  };
  
  self.input.addEventListener('keyup', self.onKeyup, false);
}

