erties related to the index.
   *
   * @param {number} index
   *   The index of the result.
   * @returns {string}
   */
  getCommentAt(index) {
    return this.#getAt(index).comment ?? "";
  }

  /**
   * Gets the style hint for the result at the given index.
   *
   * @param {number} index
   *   The index of the result.
   * @returns {string}
   */
  getStyleAt(index) {
    const itemStyle = this.#getAt(index).style;
    if (itemStyle) {
      return itemStyle;
    }

    if (index >= 0) {
      if (index < this.#formHistoryEntries.length) {
        return "fromhistory";
      }

      if (index > 0 && index == this.#formHistoryEntries.length) {
        return "datalist-first";
      }
    }
    return "";
  }

  /**
   * Gets the image of the result at the given index.
   *
   * @param {number} _index
   *   The index of the result.
   * @returns {string}
   */
  getImageAt(_index) {
    return "";
  }

  /**
   * Gets the final value that should be completed when the user confirms
   * the match at the given index.
   *
   * @param {number} index
   *   The index of the result.
   * @returns {string}
   */
  getFinalCompleteValueAt(index) {
    return this.getValueAt(index);
  }

  /**
   * True if the value at the given index is removable.
   *
   * @param {number} index
   *   The index of the result.
   * @returns {boolean}
   */
  isRemovableAt(index) {
    return this.#isFormHistoryEntry(index);
  }

  /**
   * Remove the value at the given index from the autocomplete results.
   *
   * @param {number} index
   *   The index of the result.
   */
  removeValueAt(index) {
    if (this.isRemovableAt(index)) {
      const [removedEntry] = this.#formHistoryEntries.splice(index, 1);
      lazy.FormHistory.update({
        op: "remove",
        fieldname: this.#formFieldName,
        value: removedEntry.text,
        guid: removedEntry.guid,
      });
    }
  }

  /**
   * Returns the entry at the given index taking into account both the
   * form history entries and the remote entries.
   *
   * @param {number} index
   *   The index of the entry to find.
   * @returns {object}
   *   The object at the given index.
   * @throws {Components.Exception}
   *   Throws if the index is out of range.
   */
  #getAt(index) {
    for (const group of [this.#formHistoryEntries, this.#remoteEntries]) {
      if (index < group.length) {
        return group[index];
      }
      index -= group.length;
    }

    throw Components.Exception(
      "Index out of range.",
      Cr.NS_ERROR_ILLEGAL_VALUE
    );
  }

  /**
   * Returns true if the value at the given index is one of the form history
   * entries.
   *
   * @param {number} index
   *   The index of the result.
   * @returns {boolean}
   */

  #isFormHistoryEntry(index) {
    return index >= 0 && index < this.#formHistoryEntries.length;
  }

  /**
   * Remove items from history list that are already present in fixed list.
   * We do this rather than the opposite ( i.e. remove items from fixed list)
   * to reflect the order that is specified in the fixed list.
   */
  #removeDuplicateHistoryEntries() {
    this.#formHistoryEntries = this.#formHistoryEntries.filter(entry =>
      this.#remoteEntries.every(
        fixed => entry.text != (fixed.label || fixed.value)
      )
    );
  }
}

/**
 * SuggestAutoComplete is a base class that implements nsIAutoCompleteSearch
 * and can collect results for a given search by using this.#suggestionController.
 * We do it this way since the AutoCompleteController in Mozilla requires a
 * unique XPCOM Service for every search provider, even if the logic for two
 * providers is identical.
 *
 * @implements {nsIAutoCompleteSearch}
 */
class SuggestAutoComplete {
  constructor() {
    this.#suggestionController = new lazy.SearchSuggestionController();
  }

  /**
   * Notifies the front end of new results.
   *
   * @param {nsIAutoCompleteResult} result
   *   Any previous form history result.
   * @private
   */
  onResultsReady(result) {
    if (this.#listener) {
      this.#listener.onSearchResult(this, result);

      // Null out listener to make sure we don't notify it twice
      this.#listener = null;
    }
  }

  /**
   * Initiates the search result gathering process. Part of
   * nsIAutoCompleteSearch implementation.
   *
   * @param {string} searchString
   *   The user's query string.
   * @param {string} searchParam
   *   unused, "an extra parameter"; even though this parameter and the
   *   next are unused, pass them through in case the form history
   *   service wants them
   * @param {object} previousResult
   *   unused, a client-cached store of the previous generated resultset
   *   for faster searching.
   * @param {nsIAutoCompleteObserver} listener
   *   object implementing nsIAutoCompleteObserver which we notify when
   *   results are ready.
   */
  startSearch(searchString, searchParam, previousResult, listener) {
    var formHistorySearchParam = searchParam.split("|")[0];

    // Receive the information about the privacy mode of the window to which
    // this search box belongs.  The front-end's search.xml bindings passes this
    // information in the searchParam parameter.  The alternative would have
    // been to modify nsIAutoCompleteSearch to add an argument to startSearch
    // and patch all of autocomplete to be aware of this, but the searchParam
    // argument is already an opaque argument, so this solution is hopefully
    // less hackish (although still gross.)
    var privacyMode = searchParam.split("|")[1] == "private";

    // Start search immediately if possible, otherwise once the search
    // service is initialized
    if (Services.search.isInitialized) {
      this.#triggerSearch(
        searchString,
        formHistorySearchParam,
        listener,
        privacyMode
      ).catch(console.error);
      return;
    }

    Services.search
      .init()
      .then(() => {
        this.#triggerSearch(
          searchString,
          formHistorySearchParam,
          listener,
          privacyMode
        ).catch(console.error);
      })
      .catch(result =>
        console.error(
          "Could not initialize search service, bailing out:",
          result
        )
      );
  }

  /**
   * Ends the search result gathering process. Part of nsIAutoCompleteSearch
   * implementation.
   */
  stopSearch() {
    // Prevent reporting results of stopped search
    this.#listener = null;
    this.#suggestionController.stop();
  }

  #suggestionController;

  /**
   * Maximum number of history items displayed. This is capped at 7
   * because the primary consumer (Firefox search bar) displays 10 rows
   * by default, and so we want to leave some space for suggestions
   * to be visible.
   *
   * @type {number}
   */
  #historyLimit = 7;

  /**
   * The object implementing nsIAutoCompleteObserver that we notify when
   * we have found results.
   *
   *  @type {nsIAutoCompleteObserver|null}
   */
  #listener = null;

  /**
   * Actual implementation of search.
   *
   * @param {string} searchString
   *   The user's query string.
   * @param {string} searchParam
   *   unused
   * @param {nsIAutoCompleteObserver} listener
   *   object implementing nsIAutoCompleteObserver which we notify when
   *   results are ready.
   * @param {boolean} inPrivateBrowsing
   *   True if the search was made from a private browsing mode context.
   */
  async #triggerSearch(searchString, searchParam, listener, inPrivateBrowsing) {
    this.#listener = listener;
    let results = await this.#suggestionController.fetch({
      searchString,
      inPrivateBrowsing,
      engine: Services.search.defaultEngine,
      maxLocalResults: this.#historyLimit,
    });

    let formHistoryEntries = (results?.formHistoryResults ?? []).map(
      historyEntry => ({
        // We supply the comments field so that autocomplete does not kick
        // in the unescaping of the results for display which it uses for
        // urls.
        comment: historyEntry.text,
        ...historyEntry,
      })
    );
    let autoCompleteResult = new SearchHistoryResult(
      lazy.DEFAULT_FORM_HISTORY_PARAM,
      searchString,
      formHistoryEntries
    );

    if (results?.remote?.length) {
      // We shouldn't show tail suggestions in their full-text form.
      // Suggestions are shown after form history results.
      autoCompleteResult.remoteEntries = results.remote.reduce((acc, item) => {
        if (!item.matchPrefix && !item.tail) {
          acc.push({
            value: item.value,
            label: item.value,
            // We supply the comments field so that autocomplete does not kick
            // in the unescaping of the results for display which it uses for
            // urls.
            comment: item.value,
          });
        }

        return acc;
      }, []);
    }

    // Notify the FE of our new results
    this.onResultsReady(autoCompleteResult);
  }

  QueryInterface = ChromeUtils.generateQI([
    "nsIAutoCompleteSearch",
    "nsIAutoCompleteObserver",
  ]);
}

/**
 * SearchSuggestAutoComplete is a service implementation that handles suggest
 * results specific to web searches.
 *
 * @class
 */
export class SearchSuggestAutoComplete extends SuggestAutoComplete {
  classID = Components.ID("{aa892eb4-ffbf-477d-9f9a-06c995ae9f27}");
  serviceURL = "";
}
PK