/**
 * @namespace GA_project
 * @sdoc ga.sdoc
 */
GA.binaryFind = function(arr, searchValue, indices) {
  var currValue, b;
  !indices && (indices = [0, arr.length-1]);
  while (indices[0]<=indices[1]) {
    indices[2] = indices[0] + ((indices[1]-indices[0]) >> 1);
    b = GA.binaryFind.comparator(searchValue, arr[indices[2]]);
    if (!b) return indices;
    indices[Number(b < 0)] = indices[2] + b
  }
  indices.length = 2;
  return indices
}
GA.binaryFind.comparator = function(searchValue, currValue) {
  return Number(currValue < searchValue) || -(currValue > searchValue) || 0
}
GA.autoCompliterClass = (function() {
  function outputChoiceClass(id) {
    this.Id = id;
    //прикрепляем обработчик, выбора варианта автозаполнения,
    //на клике по блоку вывода вариантов.
    this.methodIntoEventProcessing('eventProcessing', 'click', document);
    this.methodIntoEventProcessing('eventProcessing', 'focus', document);
    this.methodIntoEventProcessing('eventProcessing', 'change', document)
  }
  outputChoiceClass.prototype = {
    'methodIntoEventProcessing' :
    GA.eventWatcherClass.prototype.methodIntoEventProcessing,
    'eventProcessing': function(oNode, pos, e) {
      var initialNode = oNode;
      do {
        if (this.Id == oNode.id) {
          // событие произошло на узле с заданным id.
          if (e.type == 'focus' || e.type == 'change') {
            this.oNode.focus();
            return false
          }
          return this.onClickIn(oNode, pos, e)
        }
      } while (oNode = oNode.parentNode);
      // событие произошло вне узла с заданным id.
      if (e.type == 'click') return this.onClickOut(initialNode, pos, e)
    },
    'onClickIn': function(oNode, pos, e) {
      var tagBegin = new String(),
      tagBeginLowerCase = new String(),
      stringBeforeCurrTag, ln, s;
      //получить строку до курсора или выбранного текста.
      tagBegin = GA.ranges.getStringBeforeSelected(this.oNode);
      //получить начальные символы создаваемого тега.
      //это часть, от конца и до последней запятой (если есть запятая),
      stringBeforeCurrTag = tagBegin.split(',');
      tagBegin = stringBeforeCurrTag[stringBeforeCurrTag.length-1];
      //убрать пробелы в начале и конце.
      tagBeginLowerCase = (
        tagBegin = tagBegin.replace(/^\s+/,'')//.replace(/\s+$/,'')
      ).toLowerCase();
      ln = tagBegin.length;
      s = GA.ranges.getStringAfterSelected(this.oNode);
      if(
        //если после вставляемого продолжения есть текст не начинающийся с запятой,
        !/^\s*,/.test(s) &&
        !/^\s*$/.test(s)
      ) {
        s = ', '
      } else {
        /^\s*,/.test(s) && GA.ranges.extendSelect(',', this.oNode, 'endBefore');
        s = ''
      }
      s = oNode.value.slice(ln) + s;
      GA.ranges.setSelectedString(s, this.oNode);
      return
    },
    'onClickOut': function(oNode, pos, e) {
      var t;
      if (t = document.getElementById(this.Id)) {
        t.style.display = 'none'
      }
      return
    }
  };
  return function(inputClassName, outputChoiceId, url) {
    GA.eventWatcherClass.call(this, ['keyup', 'keydown'], inputClassName, 'onNeedCompliterChoice');
    this.outputChoice = new outputChoiceClass(outputChoiceId);
    this.requestURL = url
  }
})();
GA.autoCompliterClass.encode = function(s) {
  var i = s.length, a=[], k, t, r=[],
  base64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-/_';
  if (!i) return '';
  while (i--) {
    k = s.charCodeAt(i);
    // преобразуем Unicode в последовательность байтов UTF-8 (в обратном порядке)
    if (k < 128) {
      a[a.length] = k;
    } else {
      t = -64;
      do {
        a[a.length] = k & 63 | 128;
        k >>= 6;
        t >>= 1
      } while (k & t)
      a[a.length] = k | (t << 1) & 255;
    }
  }
  i = a.length;
  k = [0,0,0];
  while (i > 2) {
    k[0] = a[--i];
    r[r.length] = base64chars.charAt(k[0] >> 2);
    k[1] = a[--i];
    r[r.length] = base64chars.charAt( ((k[0] & 0x03) << 4) | (k[1] >> 4) );
    k[2] = a[--i];
    r[r.length] = base64chars.charAt( ((k[1] & 0x0F) << 2) | (k[2] >> 6) );
    r[r.length] = base64chars.charAt(k[2] & 0x3F);
  }
  if (i--) {
    t = base64chars.slice(-1);
    k[0] = a[i];
    r[r.length] = base64chars.charAt(k[0] >> 2);
    if (i--) {
      k[1] = a[i];
      r[r.length] = base64chars.charAt( ((k[0] & 0x03) << 4) | (k[1] >> 4) );
      r[r.length] = base64chars.charAt((k[1] & 0x0F) << 2);
    } else {
      r[r.length] = base64chars.charAt(((k[0] & 0x03) << 4));
      r[r.length] = t;
    }
    r[r.length] = t;
  }
  return r.join('')
};
GA.autoCompliterClass.prototype = {
  //количество символов, с которого начинаем запрашивать у сервера, названия тегов.
  'minStringLengthOfCompliterRequested': 2,
  //максимальное количество выводимых вариантов автозаполнения.
  'maxCountOfOutputChoises': 12,
  //массив закешированных, ранее полученных, вариантов автозаполнения.
  'aChoices': [],
  //хеш-массив начал вариантов автозаполнения, отсудствующих в базе.
  //где ключь - строка начала, значение - время последней сверки с базой.
  'aHavents': {},
  //интервал (милисекунды), после которого можно проверять не появились ли теги,
  //начинающиеся со строк хранящихся в GA.autoCompliterClass.prototype.aHavents
  'verifyTime': 15000,
  'showChoices': function(aCurrChoices, oNode, tagBegin, isFromCache) {
    var indeces, t, i, j, isBottom;
    //выводим варианты автозаплнения.
    if (i = document.getElementById(this.outputChoice.Id)) {
      if (!aCurrChoices) {
        i.style.display = 'none';
        //запоминаем, что в базе нет тегов начинающихся на эту строку, на текущий момент.
        this.aHavents[tagBegin.toLowerCase()] = (new Date()).getTime();
        return
      }
      this.outputChoice.oNode = oNode;
      j = i.firstChild;
      while (j.nodeType != 1) {j = j.nextSibling}
      j = j.cloneNode(true);
      t = i.cloneNode(false);
      i.parentNode.replaceChild(t, i);
      //t.innerHTML = '';
      for(isBottom = aCurrChoices.length, i=0; i < isBottom; i++) {
        j.value = aCurrChoices[i][0];
        j[typeof j.textContent != 'undefined'? 'textContent': 'innerText'] = j.value;// + ', '+aCurrChoices[i][1];
        t.appendChild(j.cloneNode(true))
      }
      t.style.display = '';
      t.scrollTop = 0;
      t.scrollLeft = 0;
      //сохраняем выделенную область для Оперы.
      window.opera && GA.ranges.storeSelection(oNode)
      t.selectedIndex = -1;
    }
    //если кеш пустой или содержит слишком много строк.
    t = !this.aChoices.length || !this.aChoices.length > 512;
    //или список вариантов заполнения пришел не из кеша.
    if (!isFromCache || t ) {
      //сортируем список в порядке возрастания текстового значения.
      aCurrChoices.sort(function(f, s) {
        var a = f[0].toLowerCase(),
        b = s[0].toLowerCase();
        return Number(b < a) || -(b > a) || 0
      });
    }
    //если кеш пустой или содержит слишком много строк, заполняем кеш, пришедшим списком.
    //t && (this.aChoices = aCurrChoices.slice(0));
    t && (this.aChoices = aCurrChoices);
    //если пришли новые варианты (не из кеша) и небыл только что создан из этих вариантов.
    if (!isFromCache && !t) {
      //втсавляем, в алфавитном порядке, строки в кеш , если их в нем нет,
      //диапазон индексов кеш-массива, в котором следует искать место для вставки записи по порядку.
      indeces = [0, this.aChoices.length-1];
      GA.binaryFind.comparator=function(searchValue, currValue) {
        return Number(currValue[0] < searchValue) || -(currValue[0] > searchValue) || 0
      };
      for (j = [isBottom = i = 0, aCurrChoices.length]; j[0]!=j[1]; i = j[isBottom] -= isBottom || -1) {
        t = GA.binaryFind(this.aChoices, aCurrChoices[i][0], [indeces[0], indeces[1]]);
        if (t.length < 3) {
          //если в кеше нет записей c текущим текстом, вставляем запись сохраняя порядок.
          this.aChoices.splice(t[0], 0, aCurrChoices[i]);
          indeces[isBottom] = t[isBottom]
        } else {
          //иначе заменяем значение популярности.
          this.aChoices[t[2]][1] = aCurrChoices[i][1];
          indeces[isBottom] = t[2] - isBottom
        }
        (isBottom ^= 1) && indeces[0]++
      }
    }
  },
  'onNeedCompliterChoice': function(oNode, documentPosition, e) {
    var tagBegin = new String(),
    tagBeginLowerCase = new String(),
    stringBeforeCurrTag,
    t = document.getElementById(this.outputChoice.Id), store, store2, a, ln,
    keyCodeCommand = {
      38: -1, //стрелка вверх.
      40: 1 //стрелка вниз.
    };
    if (
      typeof keyCodeCommand[e.keyCode] != 'undefined' &&
      t &&
      t.style.display == ''
    ) {
      if (e.type == 'keyup') return false
      if (e.type == 'keydown') {
        store = t.selectedIndex + keyCodeCommand[e.keyCode];
        store < 0 && (store = 0);
        store > t.options.length-1 && (store = t.options.length -1);
        t.selectedIndex = store;
        window.opera && GA.ranges.restoreSelection(oNode);
        this.outputChoice.onClickIn(t);
        window.opera && GA.ranges.storeSelection(oNode)
      }
      return false
    }
    //если нажат Enter
    if (e.type == 'keydown' && e.keyCode == 13 && t && t.style.display == '') {
      if (window.opera) {
        var b = Number(oNode.selectionStart <= oNode.selectionEnd),
        indxs = [oNode.selectionStart, oNode.selectionEnd];
        oNode.setSelectionRange(indxs[b^1]-1, indxs[b]);
        this.outputChoice.onClickIn(t);
        GA.ranges.extendSelect(',', oNode, 'endAfter');
        GA.ranges.collapse(oNode, false);
        GA.ranges.storeSelection(oNode);
        return
      } else {
        GA.ranges.extendSelect(',', oNode, 'endAfter');
        GA.ranges.collapse(oNode, false);
        return false
      }
    }
    if (e.type == 'keydown') return;
    t && (t.style.display = 'none');
    //если отпущена не символная клавиша,
    if (
      typeof {9:0,13:0,27:0,45:0,46:0,91:0,92:0,93:0,144:0,145:0,154:0,157:0}[e.keyCode] != 'undefined' ||
      (16 <= e.keyCode && e.keyCode <= 20) ||
      (33 <= e.keyCode && e.keyCode <= 40) ||
      (96 <= e.keyCode && e.keyCode <= 123) ||
      e.altKey || e.ctrlKey || e.metaKey
    ) {
      //выходим.
      return
    }
    //получить строку до курсора или выбранного текста.
    tagBegin = GA.ranges.getStringBeforeSelected(oNode);
    //получить начальные символы создаваемого тега.
    //это часть, от конца и до последней запятой (если есть запятая),
    stringBeforeCurrTag = tagBegin.split(',');
    tagBegin = stringBeforeCurrTag[stringBeforeCurrTag.length-1];
    //убрать пробелы в начале.
    tagBeginLowerCase = (
      tagBegin = tagBegin.replace(/^\s+/,'')//.replace(/\s+$/,'')
    ).toLowerCase();
    //если символов меньше чем минимум, выходим.
    if (tagBegin.length < this.minStringLengthOfCompliterRequested) {
      t && (t.style.display = 'none');
      return
    }
    ln = tagBegin.length;
    //для того чтобы создать массив тегов, начинающихся с текущей строки.
    //ищем совпадение части набранного тега, с таким же началом строк в кеше.
    //(бинарный поиск в отсортированном массиве)
    GA.binaryFind.comparator = function(searchValue, currValue) {
      return Number(currValue[0].slice(0, ln).toLowerCase() < searchValue) ||
        -(currValue[0].slice(0, ln).toLowerCase() > searchValue) || 0
    };
    t = GA.binaryFind( this.aChoices, tagBeginLowerCase);
    //если в кеше нет строк , начинающихся с таких символов.
    if (t.length < 3) {
      //если набранная строка начинаеться со строки, о которой известно, что ее нет в базе.
      store = tagBeginLowerCase;
      while (typeof this.aHavents[store] == 'undefined' && (store = store.slice(0,-1))) {}
      if (store) {
        //если прошло достаточно времени с последней проверки присудствия в базе,
        if (this.aHavents[store] + this.verifyTime < (new Date()).getTime()) {
          //удаляем информацию о том что тегов с таким началом нет в базе.
          delete this.aHavents[store]
        } else {
          //если прошло не достаточно времени, выходим.
          return
        }
      }
      //запрашиваем у сервера теги начинающиеся с такой строки.
      GA.JsHttpRequest.prototype.send.call(this,
        this.requestURL +'?startlike='+GA.autoCompliterClass.encode(tagBeginLowerCase),
        'showChoices',
        [oNode, tagBegin]
      )
      //прекращаяем выполнение метода, при получении строк от сервера,
      //будет вызван метод отображающий список вариантов.
      return
    }
    //индекс найденого совпадения.
    store2 = store = t[2];
    //список совпавших значений.
    a = [];
    while (
      //продолжаем искать совпадения вверх по списку,
      store && this.aChoices[--store][0].slice(0, ln).toLowerCase() == tagBeginLowerCase &&
      //пока ненайдем maxCountOfOutputChoises значений.
      a.length < this.maxCountOfOutputChoises
    ) {
      a[a.length] = this.aChoices[store]
    }
    a.reverse();
    a[a.length] = this.aChoices[store2];
    while (
      //продолжаем искать совпадения вниз по списку,
      (++store2) < this.aChoices.length && this.aChoices[store2][0].slice(0, ln).toLowerCase() == tagBeginLowerCase &&
      //пока ненайдем maxCountOfOutputChoises значений.
      a.length < this.maxCountOfOutputChoises
    ) {
      a[a.length] = this.aChoices[store2]
    }
    //сортируем список по убыванию популярности и возрастанию значения строки.
    a.sort(function(a,b) {
      var f = a[0].toLowerCase(),
      s = b[0].toLowerCase();
      return Number(b[1]-0 > a[1]-0) || -(b[1]-0 < a[1]-0) || Number(s < f) || -(s > f) || 0
    })
    GA.binaryFind.comparator = function(searchValue, currValue) {
      return Number(Number(currValue[1]) > Number(searchValue[1])) ||
        -(Number(currValue[1]) < Number(searchValue[1])) ||
        Number(currValue[0] < searchValue[0]) ||
        -(currValue[0] > searchValue[0]) || 0
    }
    while (
      //продолжаем искать совпадения вверх по списку, пока будут совпадения.
      store && this.aChoices[--store][0].slice(0, ln).toLowerCase() == tagBeginLowerCase
    ) {
      //если поулярность текущей записи больше самой малой в списке совпавших значений.
      if (a[a.length-1][1]-0 < this.aChoices[store][1]-0) {
        //вставляем текущую запись в список сохраняя порядок убывания популярности.
        t = GA.binaryFind(a, this.aChoices[store]);
        a.splice(t[t.length < 3 ? 0: 2] ,0 , this.aChoices[store]);
        //удаляем из списка запись с самой малой популярностью.
        a.length--
      }
    }
    store2--;
    while (
      //продолжаем искать совпадения вниз по списку, пока будут совпадения.
      (++store2) < this.aChoices.length && this.aChoices[store2][0].slice(0, ln) == tagBeginLowerCase
    ) {
      //если поулярность текущей записи больше самой малой в списке совпавших значений.
      if (a[a.length-1][1]-0 < this.aChoices[store2][1]-0) {
        //удаляем из списка запись с самой малой популярностью.
        a.length--;
        //вставляем текущую запись в список сохраняя порядок убывания популярности.
        t = GA.binaryFind(a, this.aChoices[store2]);
        a.splice(t[t.length < 3 ? 1: 2]+1 ,0 , this.aChoices[store2])
      }
    }
    //отображаем список вариантов, используя полученный диапазон из кеш-массива, отсортированный по убыванию популярности.
    this.showChoices(a, oNode, tagBegin, 1)
  }
};
for (var k in GA.eventWatcherClass.prototype) {
  GA.autoCompliterClass.prototype[k] = GA.eventWatcherClass.prototype[k]
}
