/* 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/. */

/**
 * This module exports a provider that offers input history (aka adaptive
 * history) results. These results map typed search strings to Urlbar results.
 * That way, a user can find a particular result again by typing the same
 * string.
 */

import {
  UrlbarProvider,
  UrlbarUtils,
} from "resource:///modules/UrlbarUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
  UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
  UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
  UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
});

// Sqlite result row index constants.
const QUERYINDEX = {
  URL: 0,
  TITLE: 1,
  BOOKMARKED: 2,
  BOOKMARKTITLE: 3,
  TAGS: 4,
  SWITCHTAB: 8,
};

// This SQL query fragment provides the following:
//   - whether the entry is bookmarked (QUERYINDEX_BOOKMARKED)
//   - the bookmark title, if it is a bookmark (QUERYINDEX_BOOKMARKTITLE)
//   - the tags associated with a bookmarked entry (QUERYINDEX_TAGS)
const SQL_BOOKMARK_TAGS_FRAGMENT = `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`;

const SQL_ADAPTIVE_QUERY = `/* do not warn (bug 487789) */
   SELECT h.url, h.title, ${SQL_BOOKMARK_TAGS_FRAGMENT}, h.visit_count,
          h.typed, h.id, t.open_count, h.frecency
   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_openpages_temp t
          ON t.url = h.url
         AND t.userContextId = :userContextId
   WHERE AUTOCOMPLETE_MATCH(NULL, h.url,
                            IFNULL(btitle, h.title), tags,
                            h.visit_count, h.typed, bookmarked,
                            t.open_count,
                            :matchBehavior, :searchBehavior,
                            NULL)
   ORDER BY rank DESC, h.frecency DESC
   LIMIT :maxResults`;

/**
 * Class used to create the provider.
 */
class ProviderInputHistory extends UrlbarProvider {
  /**
   * Unique name for the provider, used by the context to filter on providers.
   *
   * @returns {string}
   */
  get name() {
    return "InputHistory";
  }

  /**
   * The type of the provider, must be one of UrlbarUtils.PROVIDER_TYPE.
   *
   * @returns {UrlbarUtils.PROVIDER_TYPE}
   */
  get type() {
    return UrlbarUtils.PROVIDER_TYPE.PROFILE;
  }

  /**
   * Whether this provider should be invoked for the given context.
   * If this method returns false, the providers manager won't start a query
   * with this provider, to save on resources.
   *
   * @param {UrlbarQueryContext} queryContext The query context object
   * @returns {boolean} Whether this provider should be invoked for the search.
   */
  isActive(queryContext) {
    return (
      (lazy.UrlbarPrefs.get("suggest.history") ||
        lazy.UrlbarPrefs.get("suggest.bookmark") ||
        lazy.UrlbarPrefs.get("suggest.openpage")) &&
      !queryContext.searchMode
    );
  }

  /**
   * Starts querying. Extended classes should return a Promise resolved when the
   * provider is done searching AND returning results.
   *
   * @param {UrlbarQueryContext} queryContext The query context object
   * @param {Function} addCallback Callback invoked by the provider to add a new
   *        result. A UrlbarResult should be passed to it.
   * @returns {Promise}
   */
  async startQuery(queryContext, addCallback) {
    let instance = this.queryInstance;

    let conn = await lazy.PlacesUtils.promiseLargeCacheDBConnection();
    if (instance != this.queryInstance) {
      return;
    }

    let [query, params] = this._getAdaptiveQuery(queryContext);
    let rows = await conn.executeCached(query, params);
    if (instance != this.queryInstance) {
      return;
    }

    for (let row of rows) {
      const url = row.getResultByIndex(QUERYINDEX.URL);
      const openPageCount = row.getResultByIndex(QUERYINDEX.SWITCHTAB) || 0;
      const historyTitle = row.getResultByIndex(QUERYINDEX.TITLE) || "";
      const bookmarked = row.getResultByIndex(QUERYINDEX.BOOKMARKED);
      const bookmarkTitle = bookmarked
        ? row.getResultByIndex(QUERYINDEX.BOOKMARKTITLE)
        : null;
      const tags = row.getResultByIndex(QUERYINDEX.TAGS) || "";

      let resultTitle = historyTitle;
      if (openPageCount > 0 && lazy.UrlbarPrefs.get("suggest.openpage")) {
        if (url == queryContext.currentPage) {
          // Don't suggest switching to the current page.
          continue;
        }
        let result = new lazy.UrlbarResult(
          UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
          UrlbarUtils.RESULT_SOURCE.TABS,
          ...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
            url: [url, UrlbarUtils.HIGHLIGHT.TYPED],
            title: [resultTitle, UrlbarUtils.HIGHLIGHT.TYPED],
            icon: UrlbarUtils.getIconForUrl(url),
          })
        );
        addCallback(this, result);
        continue;
      }

      let resultSource;
      if (bookmarked && lazy.UrlbarPrefs.get("suggest.bookmark")) {
        resultSource = UrlbarUtils.RESULT_SOURCE.BOOKMARKS;
        resultTitle = bookmarkTitle || historyTitle;
      } else if (lazy.UrlbarPrefs.get("suggest.history")) {
        resultSource = UrlbarUtils.RESULT_SOURCE.HISTORY;
      } else {
        continue;
      }

      let resultTags = tags
        .split(",")
        .map(t => t.trim())
        .filter(tag => {
          let lowerCaseTag = tag.toLocaleLowerCase();
          return queryContext.tokens.some(token =>
            lowerCaseTag.includes(token.lowerCaseValue)
          );
        })
        .sort();

      let result = new lazy.UrlbarResult(
        UrlbarUtils.RESULT_TYPE.URL,
        resultSource,
        ...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
          url: [url, UrlbarUtils.HIGHLIGHT.TYPED],
          title: [resultTitle, UrlbarUtils.HIGHLIGHT.TYPED],
          tags: [resultTags, UrlbarUtils.HIGHLIGHT.TYPED],
          icon: UrlbarUtils.getIconForUrl(url),
        })
      );

      addCallback(this, result);
    }
  }

  /**
   * Obtains the query to search for adaptive results.
   *
   * @param {UrlbarQueryContext} queryContext
   *   The current queryContext.
   * @returns {Array} Contains the optimized query with which to search the
   *  database and an object containing the params to bound.
   */
  _getAdaptiveQuery(queryContext) {
    return [
      SQL_ADAPTIVE_QUERY,
      {
        parent: lazy.PlacesUtils.tagsFolderId,
        search_string: queryContext.searchString.toLowerCase(),
        matchBehavior: Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE,
        searchBehavior: lazy.UrlbarPrefs.get("defaultBehavior"),
        userContextId:
          lazy.UrlbarProviderOpenTabs.getUserContextIdForOpenPagesTable(
            queryContext.userContextId,
            queryContext.isPrivate
          ),
        maxResults: queryContext.maxResults,
      },
    ];
  }
}

export var UrlbarProviderInputHistory = new ProviderInputHistory();
