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

(function(root) {
  'use strict';

  var old, keys;

  old = root.Bloodhound;
  keys = { data: 'data', protocol: 'protocol', thumbprint: 'thumbprint' };

  // add Bloodhoud to global context
  root.Bloodhound = Bloodhound;

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

  function Bloodhound(o) {
    if (!o || (!o.local && !o.prefetch && !o.remote)) {
      $.error('one of local, prefetch, or remote is required');
    }

    this.limit = o.limit || 5;
    this.sorter = getSorter(o.sorter);
    this.dupDetector = o.dupDetector || ignoreDuplicates;

    this.local = oParser.local(o);
    this.prefetch = oParser.prefetch(o);
    this.remote = oParser.remote(o);

    this.cacheKey = this.prefetch
      ? this.prefetch.cacheKey || this.prefetch.url
      : null;

    // the backing data structure used for fast pattern matching
    this.index = new SearchIndex({
      datumTokenizer: o.datumTokenizer,
      queryTokenizer: o.queryTokenizer,
    });

    // only initialize storage if there's a cacheKey otherwise
    // loading from storage on subsequent page loads is impossible
    this.storage = this.cacheKey ? new PersistentStorage(this.cacheKey) : null;
  }

  // static methods
  // --------------

  Bloodhound.noConflict = function noConflict() {
    root.Bloodhound = old;
    return Bloodhound;
  };

  Bloodhound.tokenizers = tokenizers;

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

  _.mixin(Bloodhound.prototype, {
    // ### private

    _loadPrefetch: function loadPrefetch(o) {
      var that = this,
        serialized,
        deferred;

      if ((serialized = this._readFromStorage(o.thumbprint))) {
        this.index.bootstrap(serialized);
        deferred = $.Deferred().resolve();
      } else {
        deferred = $.ajax(o.url, o.ajax).done(handlePrefetchResponse);
      }

      return deferred;

      function handlePrefetchResponse(resp) {
        // clear to mirror the behavior of bootstrapping
        that.clear();
        that.add(o.filter ? o.filter(resp) : resp);

        that._saveToStorage(that.index.serialize(), o.thumbprint, o.ttl);
      }
    },

    _getFromRemote: function getFromRemote(query, cb) {
      var that = this,
        url,
        uriEncodedQuery;

      if (!this.transport) {
        return;
      }

      query = query || '';
      uriEncodedQuery = encodeURIComponent(query);

      url = this.remote.replace
        ? this.remote.replace(this.remote.url, query)
        : this.remote.url.replace(this.remote.wildcard, uriEncodedQuery);

      return this.transport.get(url, this.remote.ajax, handleRemoteResponse);

      function handleRemoteResponse(err, resp) {
        err ? cb([]) : cb(that.remote.filter ? that.remote.filter(resp) : resp);
      }
    },

    _cancelLastRemoteRequest: function cancelLastRemoteRequest() {
      // #149: prevents outdated rate-limited requests from being sent
      this.transport && this.transport.cancel();
    },

    _saveToStorage: function saveToStorage(data, thumbprint, ttl) {
      if (this.storage) {
        this.storage.set(keys.data, data, ttl);
        this.storage.set(keys.protocol, location.protocol, ttl);
        this.storage.set(keys.thumbprint, thumbprint, ttl);
      }
    },

    _readFromStorage: function readFromStorage(thumbprint) {
      var stored = {},
        isExpired;

      if (this.storage) {
        stored.data = this.storage.get(keys.data);
        stored.protocol = this.storage.get(keys.protocol);
        stored.thumbprint = this.storage.get(keys.thumbprint);
      }
      // the stored data is considered expired if the thumbprints
      // don't match or if the protocol it was originally stored under
      // has changed
      isExpired =
        stored.thumbprint !== thumbprint ||
        stored.protocol !== location.protocol;

      return stored.data && !isExpired ? stored.data : null;
    },

    _initialize: function initialize() {
      var that = this,
        local = this.local,
        deferred;

      deferred = this.prefetch
        ? this._loadPrefetch(this.prefetch)
        : $.Deferred().resolve();

      // make sure local is added to the index after prefetch
      local && deferred.done(addLocalToIndex);

      this.transport = this.remote ? new Transport(this.remote) : null;

      return (this.initPromise = deferred.promise());

      function addLocalToIndex() {
        // local can be a function that returns an array of datums
        that.add(_.isFunction(local) ? local() : local);
      }
    },

    // ### public

    initialize: function initialize(force) {
      return !this.initPromise || force ? this._initialize() : this.initPromise;
    },

    add: function add(data) {
      this.index.add(data);
    },

    get: function get(query, cb) {
      var that = this,
        matches = [],
        cacheHit = false;

      matches = this.index.get(query);
      matches = this.sorter(matches).slice(0, this.limit);

      matches.length < this.limit
        ? (cacheHit = this._getFromRemote(query, returnRemoteMatches))
        : this._cancelLastRemoteRequest();

      // if a cache hit occurred, skip rendering local matches
      // because the rendering of local/remote matches is already
      // in the event loop
      if (!cacheHit) {
        // only render if there are some local suggestions or we're
        // going to the network to backfill
        (matches.length > 0 || !this.transport) && cb && cb(matches);
      }

      function returnRemoteMatches(remoteMatches) {
        var matchesWithBackfill = matches.slice(0);

        _.each(remoteMatches, function(remoteMatch) {
          var isDuplicate;

          // checks for duplicates
          isDuplicate = _.some(matchesWithBackfill, function(match) {
            return that.dupDetector(remoteMatch, match);
          });

          !isDuplicate && matchesWithBackfill.push(remoteMatch);

          // if we're at the limit, we no longer need to process
          // the remote results and can break out of the each loop
          return matchesWithBackfill.length < that.limit;
        });
        cb && cb(that.sorter(matchesWithBackfill));
      }
    },

    clear: function clear() {
      this.index.reset();
    },

    clearPrefetchCache: function clearPrefetchCache() {
      this.storage && this.storage.clear();
    },

    clearRemoteCache: function clearRemoteCache() {
      this.transport && Transport.resetCache();
    },

    ttAdapter: function ttAdapter() {
      return _.bind(this.get, this);
    },
  });

  return Bloodhound;

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

  function getSorter(sortFn) {
    return _.isFunction(sortFn) ? sort : noSort;

    function sort(array) {
      return array.sort(sortFn);
    }
    function noSort(array) {
      return array;
    }
  }

  function ignoreDuplicates() {
    return false;
  }
})(this);
