/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
 * vim: sw=2 ts=2 sts=2 expandtab
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                  "resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
                                  "resource://gre/modules/TelemetryStopwatch.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                  "resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                  "resource://gre/modules/Task.jsm");

////////////////////////////////////////////////////////////////////////////////
//// Constants

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;

// This SQL query fragment provides the following:
//   - whether the entry is bookmarked (kQueryIndexBookmarked)
//   - the bookmark title, if it is a bookmark (kQueryIndexBookmarkTitle)
//   - the tags associated with a bookmarked entry (kQueryIndexTags)
const kBookTagSQLFragment =
  `EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked,
   (
     SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL
     ORDER BY lastModified DESC LIMIT 1
   ) AS btitle,
   (
     SELECT GROUP_CONCAT(t.title, ',')
     FROM moz_bookmarks b
     JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent
     WHERE b.fk = h.id
   ) AS tags`;

// observer topics
const kTopicShutdown = "places-shutdown";
const kPrefChanged = "nsPref:changed";

// Match type constants.  These indicate what type of search function we should
// be using.
const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE;
const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE;
const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING;
const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE;

// AutoComplete index constants.  All AutoComplete queries will provide these
// columns in this order.
const kQueryIndexURL = 0;
const kQueryIndexTitle = 1;
const kQueryIndexFaviconURL = 2;
const kQueryIndexBookmarked = 3;
const kQueryIndexBookmarkTitle = 4;
const kQueryIndexTags = 5;
const kQueryIndexVisitCount = 6;
const kQueryIndexTyped = 7;
const kQueryIndexPlaceId = 8;
const kQueryIndexQueryType = 9;
const kQueryIndexOpenPageCount = 10;

// AutoComplete query type constants.  Describes the various types of queries
// that we can process.
const kQueryTypeKeyword = 0;
const kQueryTypeFiltered = 1;

// This separator is used as an RTL-friendly way to split the title and tags.
// It can also be used by an nsIAutoCompleteResult consumer to re-split the
// "comment" back into the title and the tag.
const kTitleTagsSeparator = " \u2013 ";

const kBrowserUrlbarBranch = "browser.urlbar.";
// Toggle autocomplete.
const kBrowserUrlbarAutocompleteEnabledPref = "autocomplete.enabled";
// Toggle autoFill.
const kBrowserUrlbarAutofillPref = "autoFill";
// Whether to search only typed entries.
const kBrowserUrlbarAutofillTypedPref = "autoFill.typed";

// The Telemetry histogram for urlInlineComplete query on domain
const DOMAIN_QUERY_TELEMETRY = "PLACES_AUTOCOMPLETE_URLINLINE_DOMAIN_QUERY_TIME_MS";

////////////////////////////////////////////////////////////////////////////////
//// Globals

XPCOMUtils.defineLazyServiceGetter(this, "gTextURIService",
                                   "@mozilla.org/intl/texttosuburi;1",
                                   "nsITextToSubURI");

////////////////////////////////////////////////////////////////////////////////
//// Helpers

/**
 * Initializes our temporary table on a given database.
 *
 * @param aDatabase
 *        The mozIStorageConnection to set up the temp table on.
 */
function initTempTable(aDatabase)
{
  // Note: this should be kept up-to-date with the definition in
  //       nsPlacesTables.h.
  let stmt = aDatabase.createAsyncStatement(
    `CREATE TEMP TABLE moz_openpages_temp (
       url TEXT PRIMARY KEY
     , open_count INTEGER
     )`
  );
  stmt.executeAsync();
  stmt.finalize();

  // Note: this should be kept up-to-date with the definition in
  //       nsPlacesTriggers.h.
  stmt = aDatabase.createAsyncStatement(
    `CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger
     AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW
     WHEN NEW.open_count = 0
     BEGIN
       DELETE FROM moz_openpages_temp
       WHERE url = NEW.url;
     END`
  );
  stmt.executeAsync();
  stmt.finalize();
}

/**
 * Used to unescape encoded URI strings, and drop information that we do not
 * care about for searching.
 *
 * @param aURIString
 *        The text to unescape and modify.
 * @return the modified uri.
 */
function fixupSearchText(aURIString)
{
  let uri = stripPrefix(aURIString);
  return gTextURIService.unEscapeURIForUI("UTF-8", uri);
}

/**
 * Strip prefixes from the URI that we don't care about for searching.
 *
 * @param aURIString
 *        The text to modify.
 * @return the modified uri.
 */
function stripPrefix(aURIString)
{
  let uri = aURIString;

  if (uri.indexOf("http://") == 0) {
    uri = uri.slice(7);
  }
  else if (uri.indexOf("https://") == 0) {
    uri = uri.slice(8);
  }
  else if (uri.indexOf("ftp://") == 0) {
    uri = uri.slice(6);
  }

  if (uri.indexOf("www.") == 0) {
    uri = uri.slice(4);
  }
  return uri;
}

/**
 * safePrefGetter get the pref with type safety.
 * This will return the default value provided if no pref is set.
 *
 * @param aPrefBranch
 *        The nsIPrefBranch containing the required preference
 * @param aName
 *        A preference name
 * @param aDefault
 *        The preference's default value
 * @return the preference value or provided default
 */

function safePrefGetter(aPrefBranch, aName, aDefault) {
  let types = {
    boolean: "Bool",
    number: "Int",
    string: "Char"
  };
  let type = types[typeof(aDefault)];
  if (!type) {
    throw "Unknown type!";
  }

  // If the pref isn't set, we want to use the default.
  if (aPrefBranch.getPrefType(aName) == Ci.nsIPrefBranch.PREF_INVALID) {
    return aDefault;
  }
  try {
    return aPrefBranch["get" + type + "Pref"](aName);
  }
  catch (e) {
    return aDefault;
  }
}

/**
 * Whether UnifiedComplete is alive.
 */
function isUnifiedCompleteInstantiated() {
  try {
    return Components.manager.QueryInterface(Ci.nsIServiceManager)
                     .isServiceInstantiated(Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"],
                                            Ci.mozIPlacesAutoComplete);
  } catch (ex) {
    return false;
  }
}

////////////////////////////////////////////////////////////////////////////////
//// AutoCompleteStatementCallbackWrapper class

/**
 * Wraps a callback and ensures that handleCompletion is not dispatched if the
 * query is no longer tracked.
 *
 * @param aAutocomplete
 *        A reference to a nsPlacesAutoComplete.
 * @param aCallback
 *        A reference to a mozIStorageStatementCallback
 * @param aDBConnection
 *        The database connection to execute the queries on.
 */
function AutoCompleteStatementCallbackWrapper(aAutocomplete, aCallback,
                                              aDBConnection)
{
  this._autocomplete = aAutocomplete;
  this._callback = aCallback;
  this._db = aDBConnection;
}

AutoCompleteStatementCallbackWrapper.prototype = {
  //////////////////////////////////////////////////////////////////////////////
  //// mozIStorageStatementCallback

  handleResult: function ACSCW_handleResult(aResultSet)
  {
    this._callback.handleResult.apply(this._callback, arguments);
  },

  handleError: function ACSCW_handleError(aError)
  {
    this._callback.handleError.apply(this._callback, arguments);
  },

  handleCompletion: function ACSCW_handleCompletion(aReason)
  {
    // Only dispatch handleCompletion if we are not done searching and are a
    // pending search.
    if (!this._autocomplete.isSearchComplete() &&
        this._autocomplete.isPendingSearch(this._handle)) {
      this._callback.handleCompletion.apply(this._callback, arguments);
    }
  },

  //////////////////////////////////////////////////////////////////////////////
  //// AutoCompleteStatementCallbackWrapper

  /**
   * Executes the specified query asynchronously.  This object will notify
   * this._callback if we should notify (logic explained in handleCompletion).
   *
   * @param aQueries
   *        The queries to execute asynchronously.
   * @return a mozIStoragePendingStatement that can be used to cancel the
   *         queries.
   */
  executeAsync: function ACSCW_executeAsync(aQueries)
  {
    return this._handle = this._db.executeAsync(aQueries, aQueries.length,
                                                this);
  },

  //////////////////////////////////////////////////////////////////////////////
  //// nsISupports

  QueryInterface: XPCOMUtils.generateQI([
    Ci.mozIStorageStatementCallback,
  ])
};

////////////////////////////////////////////////////////////////////////////////
//// nsPlacesAutoComplete class
//// @mozilla.org/autocomplete/search;1?name=history

function nsPlacesAutoComplete()
{
  //////////////////////////////////////////////////////////////////////////////
  //// Shared Constants for Smart Getters

  // TODO bug 412736 in case of a frecency tie, break it with h.typed and
  // h.visit_count which is better than nothing.  This is slow, so not doing it
  // yet...
  function baseQuery(conditions = "") {
    let query = `SELECT h.url, h.title, f.url, ${kBookTagSQLFragment},
                        h.visit_count, h.typed, h.id, :query_type,
                        t.open_count
                 FROM moz_places h
                 LEFT JOIN moz_favicons f ON f.id = h.favicon_id
                 LEFT JOIN moz_openpages_temp t ON t.url = h.url
                 WHERE h.frecency <> 0
                   AND AUTOCOMPLETE_MATCH(:searchString, h.url,
                                          IFNULL(btitle, h.title), tags,
                                          h.visit_count, h.typed,
                                          bookmarked, t.open_count,
                                          :matchBehavior, :searchBehavior)
                 ${conditions}
                 ORDER BY h.frecency DESC, h.id DESC
                 LIMIT :maxResults`;
    return query;
  }

  //////////////////////////////////////////////////////////////////////////////
  //// Smart Getters

  XPCOMUtils.defineLazyGetter(this, "_db", function() {
    // Get a cloned, read-only version of the database.  We'll only ever write
    // to our own in-memory temp table, and having a cloned copy means we do not
    // run the risk of our queries taking longer due to the main database
    // connection performing a long-running task.
    let db = PlacesUtils.history.DBConnection.clone(true);

    // Autocomplete often fallbacks to a table scan due to lack of text indices.
    // In such cases a larger cache helps reducing IO.  The default Storage
    // value is MAX_CACHE_SIZE_BYTES in storage/mozStorageConnection.cpp.
    let stmt = db.createAsyncStatement("PRAGMA cache_size = -6144"); // 6MiB
    stmt.executeAsync();
    stmt.finalize();

    // Create our in-memory tables for tab tracking.
    initTempTable(db);

    // Populate the table with current open pages cache contents.
    if (this._openPagesCache.length > 0) {
      // Avoid getter re-entrance from the _registerOpenPageQuery lazy getter.
      let stmt = this._registerOpenPageQuery =
        db.createAsyncStatement(this._registerOpenPageQuerySQL);
      let params = stmt.newBindingParamsArray();
      for (let i = 0; i < this._openPagesCache.length; i++) {
        let bp = params.newBindingParams();
        bp.bindByName("page_url", this._openPagesCache[i]);
        params.addParams(bp);
      }
      stmt.bindParameters(params);
      stmt.executeAsync();
      stmt.finalize();
      delete this._openPagesCache;
    }

    return db;
  });

  this._customQuery = (conditions = "") => {
    return this._db.createAsyncStatement(baseQuery(conditions));
  };

  XPCOMUtils.defineLazyGetter(this, "_defaultQuery", function() {
    return this._db.createAsyncStatement(baseQuery());
  });

  XPCOMUtils.defineLazyGetter(this, "_historyQuery", function() {
    // Enforce ignoring the visit_count index, since the frecency one is much
    // faster in this case.  ANALYZE helps the query planner to figure out the
    // faster path, but it may not have run yet.
    return this._db.createAsyncStatement(baseQuery("AND +h.visit_count > 0"));
  });

  XPCOMUtils.defineLazyGetter(this, "_bookmarkQuery", function() {
    return this._db.createAsyncStatement(baseQuery("AND bookmarked"));
  });

  XPCOMUtils.defineLazyGetter(this, "_tagsQuery", function() {
    return this._db.createAsyncStatement(baseQuery("AND tags IS NOT NULL"));
  });

  XPCOMUtils.defineLazyGetter(this, "_openPagesQuery", function() {
    return this._db.createAsyncStatement(
      `SELECT t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
              :query_type, t.open_count, NULL
       FROM moz_openpages_temp t
       LEFT JOIN moz_places h ON h.url = t.url
       WHERE h.id IS NULL
         AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL,
                                NULL, NULL, NULL, t.open_count,
                                :matchBehavior, :searchBehavior)
       ORDER BY t.ROWID DESC
       LIMIT :maxResults`
    );
  });

  XPCOMUtils.defineLazyGetter(this, "_typedQuery", function() {
    return this._db.createAsyncStatement(baseQuery("AND h.typed = 1"));
  });

  XPCOMUtils.defineLazyGetter(this, "_adaptiveQuery", function() {
    return this._db.createAsyncStatement(
      `/* do not warn (bug 487789) */
       SELECT h.url, h.title, f.url, ${kBookTagSQLFragment},
              h.visit_count, h.typed, h.id, :query_type, t.open_count
       FROM (
       SELECT ROUND(
           MAX(use_count) * (1 + (input = :search_string)), 1
         ) AS rank, place_id
         FROM moz_inputhistory
         WHERE input BETWEEN :search_string AND :search_string || X'FFFF'
         GROUP BY place_id
       ) AS i
       JOIN moz_places h ON h.id = i.place_id
       LEFT JOIN moz_favicons f ON f.id = h.favicon_id
       LEFT JOIN moz_openpages_temp t ON t.url = h.url
       WHERE AUTOCOMPLETE_MATCH(NULL, h.url,
                                IFNULL(btitle, h.title), tags,
                                h.visit_count, h.typed, bookmarked,
                                t.open_count,
                                :matchBehavior, :searchBehavior)
       ORDER BY rank DESC, h.frecency DESC`
    );
  });

  XPCOMUtils.defineLazyGetter(this, "_keywordQuery", function() {
    return this._db.createAsyncStatement(
      `/* do not warn (bug 487787) */
       SELECT REPLACE(h.url, '%s', :query_string) AS search_url, h.title,
       IFNULL(f.url, (SELECT f.url
                      FROM moz_places
                      JOIN moz_favicons f ON f.id = favicon_id
                      WHERE rev_host = h.rev_host
                      ORDER BY frecency DESC
                      LIMIT 1)
       ), 1, NULL, NULL, h.visit_count, h.typed, h.id,
       :query_type, t.open_count
       FROM moz_keywords k
       JOIN moz_places h ON k.place_id = h.id
       LEFT JOIN moz_favicons f ON f.id = h.favicon_id
       LEFT JOIN moz_openpages_temp t ON t.url = search_url
       WHERE k.keyword = LOWER(:keyword)`
    );
  });

  this._registerOpenPageQuerySQL =
    `INSERT OR REPLACE INTO moz_openpages_temp (url, open_count)
       VALUES (:page_url,
         IFNULL(
           (
             SELECT open_count + 1
             FROM moz_openpages_temp
             WHERE url = :page_url
           ),
           1
         )
       )`;
  XPCOMUtils.defineLazyGetter(this, "_registerOpenPageQuery", function() {
    return this._db.createAsyncStatement(this._registerOpenPageQuerySQL);
  });

  XPCOMUtils.defineLazyGetter(this, "_unregisterOpenPageQuery", function() {
    return this._db.createAsyncStatement(
      `UPDATE moz_openpages_temp
       SET open_count = open_count - 1
       WHERE url = :page_url`
    );
  });

  //////////////////////////////////////////////////////////////////////////////
  //// Initialization

  // load preferences
  this._prefs = Cc["@mozilla.org/preferences-service;1"].
                getService(Ci.nsIPrefService).
                getBranch(kBrowserUrlbarBranch);
  this._syncEnabledPref();
  this._loadPrefs(true);

  // register observers
  this._os = Cc["@mozilla.org/observer-service;1"].
              getService(Ci.nsIObserverService);
  this._os.addObserver(this, kTopicShutdown, false);

}

nsPlacesAutoComplete.prototype = {
  //////////////////////////////////////////////////////////////////////////////
  //// nsIAutoCompleteSearch

  startSearch: function PAC_startSearch(aSearchString, aSearchParam,
                                        aPreviousResult, aListener)
  {
    // Stop the search in case the controller has not taken care of it.
    this.stopSearch();

    // Note: We don't use aPreviousResult to make sure ordering of results are
    //       consistent.  See bug 412730 for more details.

    // We want to store the original string with no leading or trailing
    // whitespace for case sensitive searches.
    this._originalSearchString = aSearchString.trim();

    this._currentSearchString =
      fixupSearchText(this._originalSearchString.toLowerCase());

    let params = new Set(aSearchParam.split(" "));
    this._enableActions = params.has("enable-actions");
    this._disablePrivateActions = params.has("disable-private-actions");

    this._listener = aListener;
    let result = Cc["@mozilla.org/autocomplete/simple-result;1"].
                 createInstance(Ci.nsIAutoCompleteSimpleResult);
    result.setSearchString(aSearchString);
    result.setListener(this);
    this._result = result;

    // If we are not enabled, we need to return now.
    if (!this._enabled) {
      this._finishSearch(true);
      return;
    }

    // Reset our search behavior to the default.
    if (this._currentSearchString) {
      this._behavior = this._defaultBehavior;
    }
    else {
      this._behavior = this._emptySearchDefaultBehavior;
    }
    // For any given search, we run up to four queries:
    // 1) keywords (this._keywordQuery)
    // 2) adaptive learning (this._adaptiveQuery)
    // 3) open pages not supported by history (this._openPagesQuery)
    // 4) query from this._getSearch
    // (1) only gets ran if we get any filtered tokens from this._getSearch,
    // since if there are no tokens, there is nothing to match, so there is no
    // reason to run the query).
    let {query, tokens} =
      this._getSearch(this._getUnfilteredSearchTokens(this._currentSearchString));
    let queries = tokens.length ?
      [this._getBoundKeywordQuery(tokens), this._getBoundAdaptiveQuery()] :
      [this._getBoundAdaptiveQuery()];

    if (this._hasBehavior("openpage")) {
      queries.push(this._getBoundOpenPagesQuery(tokens));
    }
    queries.push(query);

    // Start executing our queries.
    this._telemetryStartTime = Date.now();
    this._executeQueries(queries);

    // Set up our persistent state for the duration of the search.
    this._searchTokens = tokens;
    this._usedPlaces = {};
  },

  stopSearch: function PAC_stopSearch()
  {
    // We need to cancel our searches so we do not get any [more] results.
    // However, it's possible we haven't actually started any searches, so this
    // method may throw because this._pendingQuery may be undefined.
    if (this._pendingQuery) {
      this._stopActiveQuery();
    }

    this._finishSearch(false);
  },

  //////////////////////////////////////////////////////////////////////////////
  //// nsIAutoCompleteSimpleResultListener

  onValueRemoved: function PAC_onValueRemoved(aResult, aURISpec, aRemoveFromDB)
  {
    if (aRemoveFromDB) {
      PlacesUtils.history.removePage(NetUtil.newURI(aURISpec));
    }
  },

  //////////////////////////////////////////////////////////////////////////////
  //// mozIPlacesAutoComplete

  // If the connection has not yet been started, use this local cache.  This
  // prevents autocomplete from initing the database till the first search.
  _openPagesCache: [],
  registerOpenPage: function PAC_registerOpenPage(aURI)
  {
    if (!this._databaseInitialized) {
      this._openPagesCache.push(aURI.spec);
      return;
    }

    let stmt = this._registerOpenPageQuery;
    stmt.params.page_url = aURI.spec;
    stmt.executeAsync();
  },

  unregisterOpenPage: function PAC_unregisterOpenPage(aURI)
  {
    if (!this._databaseInitialized) {
      let index = this._openPagesCache.indexOf(aURI.spec);
      if (index != -1) {
        this._openPagesCache.splice(index, 1);
      }
      return;
    }

    let stmt = this._unregisterOpenPageQuery;
    stmt.params.page_url = aURI.spec;
    stmt.executeAsync();
  },

  //////////////////////////////////////////////////////////////////////////////
  //// mozIStorageStatementCallback

  handleResult: function PAC_handleResult(aResultSet)
  {
    let row, haveMatches = false;
    while ((row = aResultSet.getNextRow())) {
      let match = this._processRow(row);
      haveMatches = haveMatches || match;

      if (this._result.matchCount == this._maxRichResults) {
        // We have enough results, so stop running our search.
        this._stopActiveQuery();

        // And finish our search.
        this._finishSearch(true);
        return;
      }

    }

    // Notify about results if we've gotten them.
    if (haveMatches) {
      this._notifyResults(true);
    }
  },

  handleError: function PAC_handleError(aError)
  {
    Components.utils.reportError("Places AutoComplete: An async statement encountered an " +
                                 "error: " + aError.result + ", '" + aError.message + "'");
  },

  handleCompletion: function PAC_handleCompletion(aReason)
  {
    // If we have already finished our search, we should bail out early.
    if (this.isSearchComplete()) {
      return;
    }

    // If we do not have enough results, and our match type is
    // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more
    // results.
    if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE &&
        this._result.matchCount < this._maxRichResults && !this._secondPass) {
      this._secondPass = true;
      let queries = [
        this._getBoundAdaptiveQuery(MATCH_ANYWHERE),
        this._getBoundSearchQuery(MATCH_ANYWHERE, this._searchTokens),
      ];
      this._executeQueries(queries);
      return;
    }

    this._finishSearch(true);
  },

  //////////////////////////////////////////////////////////////////////////////
  //// nsIObserver

  observe: function PAC_observe(aSubject, aTopic, aData)
  {
    if (aTopic == kTopicShutdown) {
      this._os.removeObserver(this, kTopicShutdown);

      // Remove our preference observer.
      this._prefs.removeObserver("", this);
      delete this._prefs;

      // Finalize the statements that we have used.
      let stmts = [
        "_defaultQuery",
        "_historyQuery",
        "_bookmarkQuery",
        "_tagsQuery",
        "_openPagesQuery",
        "_typedQuery",
        "_adaptiveQuery",
        "_keywordQuery",
        "_registerOpenPageQuery",
        "_unregisterOpenPageQuery",
      ];
      for (let i = 0; i < stmts.length; i++) {
        // We do not want to create any query we haven't already created, so
        // see if it is a getter first.
        if (Object.getOwnPropertyDescriptor(this, stmts[i]).value !== undefined) {
          this[stmts[i]].finalize();
        }
      }

      if (this._databaseInitialized) {
        this._db.asyncClose();
      }
    }
    else if (aTopic == kPrefChanged) {
      // Avoid re-entrancy when flipping linked preferences.
      if (this._ignoreNotifications)
        return;
      this._ignoreNotifications = true;
      this._loadPrefs(false, aTopic, aData);
      this._ignoreNotifications = false;
    }
  },

  //////////////////////////////////////////////////////////////////////////////
  //// nsPlacesAutoComplete

  get _databaseInitialized() {
    return Object.getOwnPropertyDescriptor(this, "_db").value !== undefined;
  },

  /**
   * Generates the tokens used in searching from a given string.
   *
   * @param aSearchString
   *        The string to generate tokens from.
   * @return an array of tokens.
   */
  _getUnfilteredSearchTokens: function PAC_unfilteredSearchTokens(aSearchString)
  {
    // Calling split on an empty string will return an array containing one
    // empty string.  We don't want that, as it'll break our logic, so return an
    // empty array then.
    return aSearchString.length ? aSearchString.split(" ") : [];
  },

  /**
   * Properly cleans up when searching is completed.
   *
   * @param aNotify
   *        Indicates if we should notify the AutoComplete listener about our
   *        results or not.
   */
  _finishSearch: function PAC_finishSearch(aNotify)
  {
    // Notify about results if we are supposed to.
    if (aNotify) {
      this._notifyResults(false);
    }

    // Clear our state
    delete this._originalSearchString;
    delete this._currentSearchString;
    delete this._strippedPrefix;
    delete this._searchTokens;
    delete this._listener;
    delete this._result;
    delete this._usedPlaces;
    delete this._pendingQuery;
    this._secondPass = false;
    this._enableActions = false;
  },

  /**
   * Executes the given queries asynchronously.
   *
   * @param aQueries
   *        The queries to execute.
   */
  _executeQueries: function PAC_executeQueries(aQueries)
  {
    // Because we might get a handleCompletion for canceled queries, we want to
    // filter out queries we no longer care about (described in the
    // handleCompletion implementation of AutoCompleteStatementCallbackWrapper).

    // Create our wrapper object and execute the queries.
    let wrapper = new AutoCompleteStatementCallbackWrapper(this, this, this._db);
    this._pendingQuery = wrapper.executeAsync(aQueries);
  },

  /**
   * Stops executing our active query.
   */
  _stopActiveQuery: function PAC_stopActiveQuery()
  {
    this._pendingQuery.cancel();
    delete this._pendingQuery;
  },

  /**
   * Notifies the listener about results.
   *
   * @param aSearchOngoing
   *        Indicates if the search is ongoing or not.
   */
  _notifyResults: function PAC_notifyResults(aSearchOngoing)
  {
    let result = this._result;
    let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH";
    if (aSearchOngoing) {
      resultCode += "_ONGOING";
    }
    result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]);
    this._listener.onSearchResult(this, result);
    if (this._telemetryStartTime) {
      let elapsed = Date.now() - this._telemetryStartTime;
      if (elapsed > 50) {
        try {
          Services.telemetry
                  .getHistogramById("PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS")
                  .add(elapsed);
        } catch (ex) {
          Components.utils.reportError("Unable to report telemetry.");
        }
      }
      this._telemetryStartTime = null;
    }
  },

  /**
   * Synchronize suggest.* prefs with autocomplete.enabled.
   */
  _syncEnabledPref: function PAC_syncEnabledPref()
  {
    let suggestPrefs = ["suggest.history", "suggest.bookmark", "suggest.openpage"];
    let types = ["History", "Bookmark", "Openpage"];

    this._enabled = safePrefGetter(this._prefs, kBrowserUrlbarAutocompleteEnabledPref,
                                   true);
    this._suggestHistory = safePrefGetter(this._prefs, "suggest.history", true);
    this._suggestBookmark = safePrefGetter(this._prefs, "suggest.bookmark", true);
    this._suggestOpenpage = safePrefGetter(this._prefs, "suggest.openpage", true);

    if (this._enabled) {
      // If the autocomplete preference is active, activate all suggest
      // preferences only if all of them are false.
      if (types.every(type => this["_suggest" + type] == false)) {
        for (let type of suggestPrefs) {
          this._prefs.setBoolPref(type, true);
        }
      }
    } else {
      // If the preference was deactivated, deactivate all suggest preferences.
      for (let type of suggestPrefs) {
        this._prefs.setBoolPref(type, false);
      }
    }
  },

  /**
   * Loads the preferences that we care about.
   *
   * @param [optional] aRegisterObserver
   *        Indicates if the preference observer should be added or not.  The
   *        default value is false.
   * @param [optional] aTopic
   *        Observer's topic, if any.
   * @param [optional] aSubject
   *        Observer's subject, if any.
   */
  _loadPrefs: function PAC_loadPrefs(aRegisterObserver, aTopic, aData)
  {
    // Avoid race conditions with UnifiedComplete component.
    if (aData && !isUnifiedCompleteInstantiated()) {
      // Synchronize suggest.* prefs with autocomplete.enabled.
      if (aData == kBrowserUrlbarAutocompleteEnabledPref) {
        this._syncEnabledPref();
      } else if (aData.startsWith("suggest.")) {
        let suggestPrefs = ["suggest.history", "suggest.bookmark", "suggest.openpage"];
        this._prefs.setBoolPref(kBrowserUrlbarAutocompleteEnabledPref,
                                suggestPrefs.some(pref => safePrefGetter(this._prefs, pref, true)));
      }
    }

    this._enabled = safePrefGetter(this._prefs,
                                   kBrowserUrlbarAutocompleteEnabledPref,
                                   true);
    this._matchBehavior = safePrefGetter(this._prefs,
                                         "matchBehavior",
                                         MATCH_BOUNDARY_ANYWHERE);
    this._filterJavaScript = safePrefGetter(this._prefs, "filter.javascript", true);
    this._maxRichResults = safePrefGetter(this._prefs, "maxRichResults", 25);
    this._restrictHistoryToken = safePrefGetter(this._prefs,
                                                "restrict.history", "^");
    this._restrictBookmarkToken = safePrefGetter(this._prefs,
                                                 "restrict.bookmark", "*");
    this._restrictTypedToken = safePrefGetter(this._prefs, "restrict.typed", "~");
    this._restrictTagToken = safePrefGetter(this._prefs, "restrict.tag", "+");
    this._restrictOpenPageToken = safePrefGetter(this._prefs,
                                                 "restrict.openpage", "%");
    this._matchTitleToken = safePrefGetter(this._prefs, "match.title", "#");
    this._matchURLToken = safePrefGetter(this._prefs, "match.url", "@");

    this._suggestHistory = safePrefGetter(this._prefs, "suggest.history", true);
    this._suggestBookmark = safePrefGetter(this._prefs, "suggest.bookmark", true);
    this._suggestOpenpage = safePrefGetter(this._prefs, "suggest.openpage", true);
    this._suggestTyped = safePrefGetter(this._prefs, "suggest.history.onlyTyped", false);

    // If history is not set, onlyTyped value should be ignored.
    if (!this._suggestHistory) {
      this._suggestTyped = false;
    }
    let types = ["History", "Bookmark", "Openpage", "Typed"];
    this._defaultBehavior = types.reduce((memo, type) => {
      let prefValue = this["_suggest" + type];
      return memo | (prefValue &&
                     Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()]);
    }, 0);

    // Further restrictions to apply for "empty searches" (i.e. searches for "").
    // The empty behavior is typed history, if history is enabled. Otherwise,
    // it is bookmarks, if they are enabled. If both history and bookmarks are disabled,
    // it defaults to open pages.
    this._emptySearchDefaultBehavior = Ci.mozIPlacesAutoComplete.BEHAVIOR_RESTRICT;
    if (this._suggestHistory) {
      this._emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY |
                                          Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED;
    } else if (this._suggestBookmark) {
      this._emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK;
    } else {
      this._emptySearchDefaultBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_OPENPAGE;
    }

    // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE.
    if (this._matchBehavior != MATCH_ANYWHERE &&
        this._matchBehavior != MATCH_BOUNDARY &&
        this._matchBehavior != MATCH_BEGINNING) {
      this._matchBehavior = MATCH_BOUNDARY_ANYWHERE;
    }
    // register observer
    if (aRegisterObserver) {
      this._prefs.addObserver("", this, false);
    }
  },

  /**
   * Given an array of tokens, this function determines which query should be
   * ran.  It also removes any special search tokens.
   *
   * @param aTokens
   *        An array of search tokens.
   * @return an object with two properties:
   *         query: the correctly optimized, bound query to search the database
   *                with.
   *         tokens: the filtered list of tokens to search with.
   */
  _getSearch: function PAC_getSearch(aTokens)
  {
    let foundToken = false;
    let restrict = (behavior) => {
      if (!foundToken) {
        this._behavior = 0;
        this._setBehavior("restrict");
        foundToken = true;
      }
      this._setBehavior(behavior);
    };

    // Set the proper behavior so our call to _getBoundSearchQuery gives us the
    // correct query.
    for (let i = aTokens.length - 1; i >= 0; i--) {
      switch (aTokens[i]) {
        case this._restrictHistoryToken:
          restrict("history");
          break;
        case this._restrictBookmarkToken:
          restrict("bookmark");
          break;
        case this._restrictTagToken:
          restrict("tag");
          break;
        case this._restrictOpenPageToken:
          if (!this._enableActions) {
            continue;
          }
          restrict("openpage");
          break;
        case this._matchTitleToken:
          restrict("title");
          break;
        case this._matchURLToken:
          restrict("url");
          break;
        case this._restrictTypedToken:
          restrict("typed");
          break;
        default:
          // We do not want to remove the token if we did not match.
          continue;
      }

      aTokens.splice(i, 1);
    }

    // Set the right JavaScript behavior based on our preference.  Note that the
    // preference is whether or not we should filter JavaScript, and the
    // behavior is if we should search it or not.
    if (!this._filterJavaScript) {
      this._setBehavior("javascript");
    }

    return {
      query: this._getBoundSearchQuery(this._matchBehavior, aTokens),
      tokens: aTokens
    };
  },

  /**
   * @return a string consisting of the search query to be used based on the
   * previously set urlbar suggestion preferences.
   */
  _getSuggestionPrefQuery: function PAC_getSuggestionPrefQuery()
  {
    if (!this._hasBehavior("restrict") && this._hasBehavior("history") &&
        this._hasBehavior("bookmark")) {
      return this._hasBehavior("typed") ? this._customQuery("AND h.typed = 1")
                                        : this._defaultQuery;
    }
    let conditions = [];
    if (this._hasBehavior("history")) {
      // Enforce ignoring the visit_count index, since the frecency one is much
      // faster in this case.  ANALYZE helps the query planner to figure out the
      // faster path, but it may not have up-to-date information yet.
      conditions.push("+h.visit_count > 0");
    }
    if (this._hasBehavior("typed")) {
      conditions.push("h.typed = 1");
    }
    if (this._hasBehavior("bookmark")) {
      conditions.push("bookmarked");
    }
    if (this._hasBehavior("tag")) {
      conditions.push("tags NOTNULL");
    }

    return conditions.length ? this._customQuery("AND " + conditions.join(" AND "))
                             : this._defaultQuery;
  },

  /**
   * Obtains the search query to be used based on the previously set search
   * behaviors (accessed by this._hasBehavior).  The query is bound and ready to
   * execute.
   *
   * @param aMatchBehavior
   *        How this query should match its tokens to the search string.
   * @param aTokens
   *        An array of search tokens.
   * @return the correctly optimized query to search the database with and the
   *         new list of tokens to search with.  The query has all the needed
   *         parameters bound, so consumers can execute it without doing any
   *         additional work.
   */
  _getBoundSearchQuery: function PAC_getBoundSearchQuery(aMatchBehavior,
                                                         aTokens)
  {
    let query = this._getSuggestionPrefQuery();

    // Bind the needed parameters to the query so consumers can use it.
    let params = query.params;
    params.parent = PlacesUtils.tagsFolderId;
    params.query_type = kQueryTypeFiltered;
    params.matchBehavior = aMatchBehavior;
    params.searchBehavior = this._behavior;

    // We only want to search the tokens that we are left with - not the
    // original search string.
    params.searchString = aTokens.join(" ");

    // Limit the query to the the maximum number of desired results.
    // This way we can avoid doing more work than needed.
    params.maxResults = this._maxRichResults;

    return query;
  },

  _getBoundOpenPagesQuery: function PAC_getBoundOpenPagesQuery(aTokens)
  {
    let query = this._openPagesQuery;

    // Bind the needed parameters to the query so consumers can use it.
    let params = query.params;
    params.query_type = kQueryTypeFiltered;
    params.matchBehavior = this._matchBehavior;
    params.searchBehavior = this._behavior;

    // We only want to search the tokens that we are left with - not the
    // original search string.
    params.searchString = aTokens.join(" ");
    params.maxResults = this._maxRichResults;

    return query;
  },

  /**
   * Obtains the keyword query with the properly bound parameters.
   *
   * @param aTokens
   *        The array of search tokens to check against.
   * @return the bound keyword query.
   */
  _getBoundKeywordQuery: function PAC_getBoundKeywordQuery(aTokens)
  {
    // The keyword is the first word in the search string, with the parameters
    // following it.
    let searchString = this._originalSearchString;
    let queryString = "";
    let queryIndex = searchString.indexOf(" ");
    if (queryIndex != -1) {
      queryString = searchString.substring(queryIndex + 1);
    }
    // We need to escape the parameters as if they were the query in a URL
    queryString = encodeURIComponent(queryString).replace(/%20/g, "+");

    // The first word could be a keyword, so that's what we'll search.
    let keyword = aTokens[0];

    let query = this._keywordQuery;
    let params = query.params;
    params.keyword = keyword;
    params.query_string = queryString;
    params.query_type = kQueryTypeKeyword;

    return query;
  },

  /**
   * Obtains the adaptive query with the properly bound parameters.
   *
   * @return the bound adaptive query.
   */
  _getBoundAdaptiveQuery: function PAC_getBoundAdaptiveQuery(aMatchBehavior)
  {
    // If we were not given a match behavior, use the stored match behavior.
    if (arguments.length == 0) {
      aMatchBehavior = this._matchBehavior;
    }

    let query = this._adaptiveQuery;
    let params = query.params;
    params.parent = PlacesUtils.tagsFolderId;
    params.search_string = this._currentSearchString;
    params.query_type = kQueryTypeFiltered;
    params.matchBehavior = aMatchBehavior;
    params.searchBehavior = this._behavior;

    return query;
  },

  /**
   * Processes a mozIStorageRow to generate the proper data for the AutoComplete
   * result.  This will add an entry to the current result if it matches the
   * criteria.
   *
   * @param aRow
   *        The row to process.
   * @return true if the row is accepted, and false if not.
   */
  _processRow: function PAC_processRow(aRow)
  {
    // Before we do any work, make sure this entry isn't already in our results.
    let entryId = aRow.getResultByIndex(kQueryIndexPlaceId);
    let escapedEntryURL = aRow.getResultByIndex(kQueryIndexURL);
    let openPageCount = aRow.getResultByIndex(kQueryIndexOpenPageCount) || 0;

    // If actions are enabled and the page is open, add only the switch-to-tab
    // result.  Otherwise, add the normal result.
    let [url, action] = this._enableActions && openPageCount > 0 && this._hasBehavior("openpage") ?
                        ["moz-action:switchtab," + escapedEntryURL, "action "] :
                        [escapedEntryURL, ""];

    if (this._inResults(entryId, url)) {
      return false;
    }

    let entryTitle = aRow.getResultByIndex(kQueryIndexTitle) || "";
    let entryFavicon = aRow.getResultByIndex(kQueryIndexFaviconURL) || "";
    let entryBookmarked = aRow.getResultByIndex(kQueryIndexBookmarked);
    let entryBookmarkTitle = entryBookmarked ?
      aRow.getResultByIndex(kQueryIndexBookmarkTitle) : null;
    let entryTags = aRow.getResultByIndex(kQueryIndexTags) || "";

    // Always prefer the bookmark title unless it is empty
    let title = entryBookmarkTitle || entryTitle;

    let style;
    if (aRow.getResultByIndex(kQueryIndexQueryType) == kQueryTypeKeyword) {
      style = "keyword";
      title = NetUtil.newURI(escapedEntryURL).host;
    }

    // We will always prefer to show tags if we have them.
    let showTags = !!entryTags;

    // However, we'll act as if a page is not bookmarked if the user wants
    // only history and not bookmarks and there are no tags.
    if (this._hasBehavior("history") && !this._hasBehavior("bookmark") &&
        !showTags) {
      showTags = false;
      style = "favicon";
    }

    // If we have tags and should show them, we need to add them to the title.
    if (showTags) {
      title += kTitleTagsSeparator + entryTags;
    }
    // We have to determine the right style to display.  Tags show the tag icon,
    // bookmarks get the bookmark icon, and keywords get the keyword icon.  If
    // the result does not fall into any of those, it just gets the favicon.
    if (!style) {
      // It is possible that we already have a style set (from a keyword
      // search or because of the user's preferences), so only set it if we
      // haven't already done so.
      if (showTags) {
        style = "tag";
      }
      else if (entryBookmarked) {
        style = "bookmark";
      }
      else {
        style = "favicon";
      }
    }

    this._addToResults(entryId, url, title, entryFavicon, action + style);
    return true;
  },

  /**
   * Checks to see if the given place has already been added to the results.
   *
   * @param aPlaceId
   *        The place id to check for, may be null.
   * @param aUrl
   *        The url to check for.
   * @return true if the place has been added, false otherwise.
   *
   * @note Must check both the id and the url for a negative match, since
   *       autocomplete may run in the middle of a new page addition.  In such
   *       a case the switch-to-tab query would hash the page by url, then a
   *       next query, running after the page addition, would hash it by id.
   *       It's not possible to just rely on url though, since keywords
   *       dynamically modify the url to include their search string.
   */
  _inResults: function PAC_inResults(aPlaceId, aUrl)
  {
    if (aPlaceId && aPlaceId in this._usedPlaces) {
      return true;
    }
    return aUrl in this._usedPlaces;
  },

  /**
   * Adds a result to the AutoComplete results.  Also tracks that we've added
   * this place_id into the result set.
   *
   * @param aPlaceId
   *        The place_id of the item to be added to the result set.  This is
   *        used by _inResults.
   * @param aURISpec
   *        The URI spec for the entry.
   * @param aTitle
   *        The title to give the entry.
   * @param aFaviconSpec
   *        The favicon to give to the entry.
   * @param aStyle
   *        Indicates how the entry should be styled when displayed.
   */
  _addToResults: function PAC_addToResults(aPlaceId, aURISpec, aTitle,
                                           aFaviconSpec, aStyle)
  {
    // Add this to our internal tracker to ensure duplicates do not end up in
    // the result.  _usedPlaces is an Object that is being used as a set.
    // Not all entries have a place id, thus we fallback to the url for them.
    // We cannot use only the url since keywords entries are modified to
    // include the search string, and would be returned multiple times.  Ids
    // are faster too.
    this._usedPlaces[aPlaceId || aURISpec] = true;

    // Obtain the favicon for this URI.
    let favicon;
    if (aFaviconSpec) {
      let uri = NetUtil.newURI(aFaviconSpec);
      favicon = PlacesUtils.favicons.getFaviconLinkForIcon(uri).spec;
    }
    favicon = favicon || PlacesUtils.favicons.defaultFavicon.spec;

    this._result.appendMatch(aURISpec, aTitle, favicon, aStyle);
  },

  /**
   * Determines if the specified AutoComplete behavior is set.
   *
   * @param aType
   *        The behavior type to test for.
   * @return true if the behavior is set, false otherwise.
   */
  _hasBehavior: function PAC_hasBehavior(aType)
  {
    let behavior = Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()];

    if (this._disablePrivateActions &&
        behavior == Ci.mozIPlacesAutoComplete.BEHAVIOR_OPENPAGE) {
      return false;
    }

    return this._behavior & behavior;
  },

  /**
   * Enables the desired AutoComplete behavior.
   *
   * @param aType
   *        The behavior type to set.
   */
  _setBehavior: function PAC_setBehavior(aType)
  {
    this._behavior |=
      Ci.mozIPlacesAutoComplete["BEHAVIOR_" + aType.toUpperCase()];
  },

  /**
   * Determines if we are done searching or not.
   *
   * @return true if we have completed searching, false otherwise.
   */
  isSearchComplete: function PAC_isSearchComplete()
  {
    // If _pendingQuery is null, we should no longer do any work since we have
    // already called _finishSearch.  This means we completed our search.
    return this._pendingQuery == null;
  },

  /**
   * Determines if the given handle of a pending statement is a pending search
   * or not.
   *
   * @param aHandle
   *        A mozIStoragePendingStatement to check and see if we are waiting for
   *        results from it still.
   * @return true if it is a pending query, false otherwise.
   */
  isPendingSearch: function PAC_isPendingSearch(aHandle)
  {
    return this._pendingQuery == aHandle;
  },

  //////////////////////////////////////////////////////////////////////////////
  //// nsISupports

  classID: Components.ID("d0272978-beab-4adc-a3d4-04b76acfa4e7"),

  _xpcom_factory: XPCOMUtils.generateSingletonFactory(nsPlacesAutoComplete),

  QueryInterface: XPCOMUtils.generateQI([
    Ci.nsIAutoCompleteSearch,
    Ci.nsIAutoCompleteSimpleResultListener,
    Ci.mozIPlacesAutoComplete,
    Ci.mozIStorageStatementCallback,
    Ci.nsIObserver,
    Ci.nsISupportsWeakReference,
  ])
};

////////////////////////////////////////////////////////////////////////////////
//// urlInlineComplete class
//// component @mozilla.org/autocomplete/search;1?name=urlinline

function urlInlineComplete()
{
  this._loadPrefs(true);
  Services.obs.addObserver(this, kTopicShutdown, true);
}

urlInlineComplete.prototype = {

/////////////////////////////////////////////////////////////////////////////////
//// Database and query getters

  __db: null,

  get _db()
  {
    if (!this.__db && this._autofillEnabled) {
      this.__db = PlacesUtils.history.DBConnection.clone(true);
    }
    return this.__db;
  },

  __hostQuery: null,

  get _hostQuery()
  {
    if (!this.__hostQuery) {
      // Add a trailing slash at the end of the hostname, since we always
      // want to complete up to and including a URL separator.
      this.__hostQuery = this._db.createAsyncStatement(
        `/* do not warn (bug no): could index on (typed,frecency) but not worth it */
         SELECT host || '/', prefix || host || '/'
         FROM moz_hosts
         WHERE host BETWEEN :search_string AND :search_string || X'FFFF'
         AND frecency <> 0
         ${this._autofillTyped ? "AND typed = 1" : ""}
         ORDER BY frecency DESC
         LIMIT 1`
      );
    }
    return this.__hostQuery;
  },

  __urlQuery: null,

  get _urlQuery()
  {
    if (!this.__urlQuery) {
      this.__urlQuery = this._db.createAsyncStatement(
        `/* do not warn (bug no): can't use an index */
         SELECT h.url
         FROM moz_places h
         WHERE h.frecency <> 0
         ${this._autofillTyped ? "AND h.typed = 1 " : ""}
           AND AUTOCOMPLETE_MATCH(:searchString, h.url,
                                  h.title, '',
                                  h.visit_count, h.typed, 0, 0,
                                  :matchBehavior, :searchBehavior)
         ORDER BY h.frecency DESC, h.id DESC
         LIMIT 1`
      );
    }
    return this.__urlQuery;
  },

  //////////////////////////////////////////////////////////////////////////////
  //// nsIAutoCompleteSearch

  startSearch: function UIC_startSearch(aSearchString, aSearchParam,
                                        aPreviousResult, aListener)
  {
    // Stop the search in case the controller has not taken care of it.
    if (this._pendingQuery) {
      this.stopSearch();
    }

    let pendingSearch = this._pendingSearch = {};

    // We want to store the original string with no leading or trailing
    // whitespace for case sensitive searches.
    this._originalSearchString = aSearchString;
    this._currentSearchString =
      fixupSearchText(this._originalSearchString.toLowerCase());
    // The protocol and the host are lowercased by nsIURI, so it's fine to
    // lowercase the typed prefix to add it back to the results later.
    this._strippedPrefix = this._originalSearchString.slice(
      0, this._originalSearchString.length - this._currentSearchString.length
    ).toLowerCase();

    this._result = Cc["@mozilla.org/autocomplete/simple-result;1"].
                   createInstance(Ci.nsIAutoCompleteSimpleResult);
    this._result.setSearchString(aSearchString);
    this._result.setTypeAheadResult(true);

    this._listener = aListener;

    Task.spawn(function* () {
      // Don't autoFill if the search term is recognized as a keyword, otherwise
      // it will override default keywords behavior.  Note that keywords are
      // hashed on first use, so while the first query may delay a little bit,
      // next ones will just hit the memory hash.
      let dontAutoFill = this._currentSearchString.length == 0 || !this._db ||
                         (yield PlacesUtils.keywords.fetch(this._currentSearchString));
      if (this._pendingSearch != pendingSearch)
        return;
      if (dontAutoFill) {
        this._finishSearch();
        return;
      }

      // Don't try to autofill if the search term includes any whitespace.
      // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH
      // tokenizer ends up trimming the search string and returning a value
      // that doesn't match it, or is even shorter.
      if (/\s/.test(this._currentSearchString)) {
        this._finishSearch();
        return;
      }

      // Hosts have no "/" in them.
      let lastSlashIndex = this._currentSearchString.lastIndexOf("/");

      // Search only URLs if there's a slash in the search string...
      if (lastSlashIndex != -1) {
        // ...but not if it's exactly at the end of the search string.
        if (lastSlashIndex < this._currentSearchString.length - 1)
          this._queryURL();
        else
          this._finishSearch();
        return;
      }

      // Do a synchronous search on the table of hosts.
      let query = this._hostQuery;
      query.params.search_string = this._currentSearchString.toLowerCase();
      // This is just to measure the delay to reach the UI, not the query time.
      TelemetryStopwatch.start(DOMAIN_QUERY_TELEMETRY);
      let wrapper = new AutoCompleteStatementCallbackWrapper(this, {
        handleResult: aResultSet => {
          if (this._pendingSearch != pendingSearch)
            return;
          let row = aResultSet.getNextRow();
          let trimmedHost = row.getResultByIndex(0);
          let untrimmedHost = row.getResultByIndex(1);
          // If the untrimmed value doesn't preserve the user's input just
          // ignore it and complete to the found host.
          if (untrimmedHost &&
              !untrimmedHost.toLowerCase().includes(this._originalSearchString.toLowerCase())) {
            untrimmedHost = null;
          }

          this._result.appendMatch(this._strippedPrefix + trimmedHost, "", "", "", untrimmedHost);

          // handleCompletion() will cause the result listener to be called, and
          // will display the result in the UI.
        },

        handleError: aError => {
          Components.utils.reportError(
            "URL Inline Complete: An async statement encountered an " +
            "error: " + aError.result + ", '" + aError.message + "'");
        },

        handleCompletion: aReason => {
          if (this._pendingSearch != pendingSearch)
            return;
          TelemetryStopwatch.finish(DOMAIN_QUERY_TELEMETRY);
          this._finishSearch();
        }
      }, this._db);
      this._pendingQuery = wrapper.executeAsync([query]);
    }.bind(this));
  },

  /**
   * Execute an asynchronous search through places, and complete
   * up to the next URL separator.
   */
  _queryURL: function UIC__queryURL()
  {
    // The URIs in the database are fixed up, so we can match on a lowercased
    // host, but the path must be matched in a case sensitive way.
    let pathIndex =
      this._originalSearchString.indexOf("/", this._strippedPrefix.length);
    this._currentSearchString = fixupSearchText(
      this._originalSearchString.slice(0, pathIndex).toLowerCase() +
      this._originalSearchString.slice(pathIndex)
    );

    // Within the standard autocomplete query, we only search the beginning
    // of URLs for 1 result.
    let query = this._urlQuery;
    let params = query.params;
    params.matchBehavior = MATCH_BEGINNING_CASE_SENSITIVE;
    params.searchBehavior |= Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY |
                             Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED |
                             Ci.mozIPlacesAutoComplete.BEHAVIOR_URL;
    params.searchString = this._currentSearchString;

    // Execute the query.
    let wrapper = new AutoCompleteStatementCallbackWrapper(this, {
      handleResult: aResultSet => {
        let row = aResultSet.getNextRow();
        let value = row.getResultByIndex(0);
        let url = fixupSearchText(value);

        let prefix = value.slice(0, value.length - stripPrefix(value).length);

        // We must complete the URL up to the next separator (which is /, ? or #).
        let separatorIndex = url.slice(this._currentSearchString.length)
                                .search(/[\/\?\#]/);
        if (separatorIndex != -1) {
          separatorIndex += this._currentSearchString.length;
          if (url[separatorIndex] == "/") {
            separatorIndex++; // Include the "/" separator
          }
          url = url.slice(0, separatorIndex);
        }

        // Add the result.
        // If the untrimmed value doesn't preserve the user's input just
        // ignore it and complete to the found url.
        let untrimmedURL = prefix + url;
        if (untrimmedURL &&
            !untrimmedURL.toLowerCase().includes(this._originalSearchString.toLowerCase())) {
          untrimmedURL = null;
         }

        this._result.appendMatch(this._strippedPrefix + url, "", "", "", untrimmedURL);

        // handleCompletion() will cause the result listener to be called, and
        // will display the result in the UI.
      },

      handleError: aError => {
        Components.utils.reportError(
          "URL Inline Complete: An async statement encountered an " +
          "error: " + aError.result + ", '" + aError.message + "'");
      },

      handleCompletion: aReason => {
        this._finishSearch();
      }
    }, this._db);
    this._pendingQuery = wrapper.executeAsync([query]);
  },

  stopSearch: function UIC_stopSearch()
  {
    delete this._originalSearchString;
    delete this._currentSearchString;
    delete this._result;
    delete this._listener;
    delete this._pendingSearch;

    if (this._pendingQuery) {
      this._pendingQuery.cancel();
      delete this._pendingQuery;
    }
  },

  /**
   * Loads the preferences that we care about.
   *
   * @param [optional] aRegisterObserver
   *        Indicates if the preference observer should be added or not.  The
   *        default value is false.
   */
  _loadPrefs: function UIC_loadPrefs(aRegisterObserver)
  {
    let prefBranch = Services.prefs.getBranch(kBrowserUrlbarBranch);
    let autocomplete = safePrefGetter(prefBranch,
                                      kBrowserUrlbarAutocompleteEnabledPref,
                                      true);
    let autofill = safePrefGetter(prefBranch,
                                  kBrowserUrlbarAutofillPref,
                                  true);
    this._autofillEnabled = autocomplete && autofill;
    this._autofillTyped = safePrefGetter(prefBranch,
                                         kBrowserUrlbarAutofillTypedPref,
                                         true);
    if (aRegisterObserver) {
      Services.prefs.addObserver(kBrowserUrlbarBranch, this, true);
    }
  },

  //////////////////////////////////////////////////////////////////////////////
  //// nsIAutoCompleteSearchDescriptor

  get searchType() {
    return Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE;
  },

  get clearingAutoFillSearchesAgain() {
    return false;
  },

  //////////////////////////////////////////////////////////////////////////////
  //// nsIObserver

  observe: function UIC_observe(aSubject, aTopic, aData)
  {
    if (aTopic == kTopicShutdown) {
      this._closeDatabase();
    }
    else if (aTopic == kPrefChanged &&
             (aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutofillPref ||
              aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutocompleteEnabledPref ||
              aData.substr(kBrowserUrlbarBranch.length) == kBrowserUrlbarAutofillTypedPref)) {
      let previousAutofillTyped = this._autofillTyped;
      this._loadPrefs();
      if (!this._autofillEnabled) {
        this.stopSearch();
        this._closeDatabase();
      }
      else if (this._autofillTyped != previousAutofillTyped) {
        // Invalidate the statements to update them for the new typed status.
        this._invalidateStatements();
      }
    }
  },

  /**
   * Finalizes and invalidates cached statements.
   */
  _invalidateStatements: function UIC_invalidateStatements()
  {
    // Finalize the statements that we have used.
    let stmts = [
      "__hostQuery",
      "__urlQuery",
    ];
    for (let i = 0; i < stmts.length; i++) {
      // We do not want to create any query we haven't already created, so
      // see if it is a getter first.
      if (this[stmts[i]]) {
        this[stmts[i]].finalize();
        this[stmts[i]] = null;
      }
    }
  },

  /**
   * Closes the database.
   */
  _closeDatabase: function UIC_closeDatabase()
  {
    this._invalidateStatements();
    if (this.__db) {
      this._db.asyncClose();
      this.__db = null;
    }
  },

  //////////////////////////////////////////////////////////////////////////////
  //// urlInlineComplete

  _finishSearch: function UIC_finishSearch()
  {
    // Notify the result object
    let result = this._result;

    if (result.matchCount) {
      result.setDefaultIndex(0);
      result.setSearchResult(Ci.nsIAutoCompleteResult["RESULT_SUCCESS"]);
    } else {
      result.setDefaultIndex(-1);
      result.setSearchResult(Ci.nsIAutoCompleteResult["RESULT_NOMATCH"]);
    }

    this._listener.onSearchResult(this, result);
    this.stopSearch();
  },

  isSearchComplete: function UIC_isSearchComplete()
  {
    return this._pendingQuery == null;
  },

  isPendingSearch: function UIC_isPendingSearch(aHandle)
  {
    return this._pendingQuery == aHandle;
  },

  //////////////////////////////////////////////////////////////////////////////
  //// nsISupports

  classID: Components.ID("c88fae2d-25cf-4338-a1f4-64a320ea7440"),

  _xpcom_factory: XPCOMUtils.generateSingletonFactory(urlInlineComplete),

  QueryInterface: XPCOMUtils.generateQI([
    Ci.nsIAutoCompleteSearch,
    Ci.nsIAutoCompleteSearchDescriptor,
    Ci.nsIObserver,
    Ci.nsISupportsWeakReference,
  ])
};

var components = [nsPlacesAutoComplete, urlInlineComplete];
this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
