/*
 * typeahead.js
 * https://github.com/twitter/typeahead.js
 * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT
 */

var SearchIndex = (function() {
  'use strict';

  // constructor
  // -----------

  function SearchIndex(o) {
    o = o || {};

    if (!o.datumTokenizer || !o.queryTokenizer) {
      $.error('datumTokenizer and queryTokenizer are both required');
    }

    this.datumTokenizer = o.datumTokenizer;
    this.queryTokenizer = o.queryTokenizer;

    this.reset();
  }

  // instance methods
  // ----------------

  _.mixin(SearchIndex.prototype, {
    // ### public

    bootstrap: function bootstrap(o) {
      this.datums = o.datums;
      this.trie = o.trie;
    },

    add: function(data) {
      var that = this;

      data = _.isArray(data) ? data : [data];

      _.each(data, function(datum) {
        var id, tokens;

        id = that.datums.push(datum) - 1;
        tokens = normalizeTokens(that.datumTokenizer(datum));

        _.each(tokens, function(token) {
          var node, chars, ch;

          node = that.trie;
          chars = token.split('');

          while ((ch = chars.shift())) {
            node = node.children[ch] || (node.children[ch] = newNode());
            node.ids.push(id);
          }
        });
      });
    },

    get: function get(query) {
      var that = this,
        tokens,
        matches;

      tokens = normalizeTokens(this.queryTokenizer(query));

      _.each(tokens, function(token) {
        var node, chars, ch, ids;

        // previous tokens didn't share any matches
        if (matches && matches.length === 0) {
          return false;
        }

        node = that.trie;
        chars = token.split('');

        while (node && (ch = chars.shift())) {
          node = node.children[ch];
        }

        if (node && chars.length === 0) {
          ids = node.ids.slice(0);
          matches = matches ? getIntersection(matches, ids) : ids;
        } else {
          // break early if we find out there are no possible matches
          matches = [];
          return false;
        }
      });

      return matches
        ? _.map(unique(matches), function(id) {
            return that.datums[id];
          })
        : [];
    },

    reset: function reset() {
      this.datums = [];
      this.trie = newNode();
    },

    serialize: function serialize() {
      return { datums: this.datums, trie: this.trie };
    },
  });

  return SearchIndex;

  // helper functions
  // ----------------

  function normalizeTokens(tokens) {
    // filter out falsy tokens
    tokens = _.filter(tokens, function(token) {
      return !!token;
    });

    // normalize tokens
    tokens = _.map(tokens, function(token) {
      return token.toLowerCase();
    });

    return tokens;
  }

  function newNode() {
    return { ids: [], children: {} };
  }

  function unique(array) {
    var seen = {},
      uniques = [];

    for (var i = 0, len = array.length; i < len; i++) {
      if (!seen[array[i]]) {
        seen[array[i]] = true;
        uniques.push(array[i]);
      }
    }

    return uniques;
  }

  function getIntersection(arrayA, arrayB) {
    var ai = 0,
      bi = 0,
      intersection = [];

    arrayA = arrayA.sort(compare);
    arrayB = arrayB.sort(compare);

    var lenArrayA = arrayA.length,
      lenArrayB = arrayB.length;

    while (ai < lenArrayA && bi < lenArrayB) {
      if (arrayA[ai] < arrayB[bi]) {
        ai++;
      } else if (arrayA[ai] > arrayB[bi]) {
        bi++;
      } else {
        intersection.push(arrayA[ai]);
        ai++;
        bi++;
      }
    }

    return intersection;

    function compare(a, b) {
      return a - b;
    }
  }
})();
