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

/**
 * @import MozButton from "chrome://global/content/elements/moz-button.mjs";
 * @import { SearchEngine } from "moz-src:///toolkit/components/search/SearchEngine.sys.mjs"
 * @import { OpenSearchData } from "moz-src:///browser/components/search/OpenSearchManager.sys.mjs"
 * @import { LocalSearchMode } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs"
 * @import { PanelItem, PanelList } from "chrome://global/content/elements/panel-list.mjs"
 */
const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  AppProvidedConfigEngine:
    "moz-src:///toolkit/components/search/ConfigSearchEngine.sys.mjs",
  BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
  ConfigSearchEngine:
    "moz-src:///toolkit/components/search/ConfigSearchEngine.sys.mjs",
  OpenSearchManager:
    "moz-src:///browser/components/search/OpenSearchManager.sys.mjs",
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
  SearchService: "moz-src:///toolkit/components/search/SearchService.sys.mjs",
  SearchUIUtils: "moz-src:///browser/components/search/SearchUIUtils.sys.mjs",
  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
  UrlbarSearchUtils:
    "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs",
  UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
});

ChromeUtils.defineLazyGetter(lazy, "SearchModeSwitcherL10n", () => {
  return new Localization(["browser/browser.ftl"]);
});

const { XPCOMUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs"
);

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "settingsRedesignEnabled",
  "browser.settings-redesign.enabled",
  true
);

// Default icon used for engines that do not have icons loaded.
const DEFAULT_ENGINE_ICON =
  "chrome://browser/skin/search-engine-placeholder@2x.png";

/**
 * Implements the SearchModeSwitcher in the urlbar.
 */
export class SearchModeSwitcher {
  static ICON_GLASS = lazy.UrlbarUtils.ICON.SEARCH_GLASS;
  static ICON_GLOBE = lazy.UrlbarUtils.ICON.GLOBE;
  /**
   * The maximum number of openSearch engines available to install
   * to display.
   */
  static MAX_OPENSEARCH_ENGINES = 3;

  /** @type {PanelList} */
  #panelList;
  /** @type {UrlbarInput} */
  #input;
  /** @type {MozButton} */
  #button;
  /** @type {HTMLButtonElement} */
  #closebutton;

  // Keep a cache of the engine list as the keyboard functionality
  // needs sync access to them.
  #engines = [];
  // Keep track of the currently selected engine when the user is cycling
  // through them with Accel+Up/Down.
  #selectedIndex = 0;

  /**
   * @param {UrlbarInput} input
   */
  constructor(input) {
    this.#input = input;

    this.QueryInterface = ChromeUtils.generateQI([
      "nsIObserver",
      "nsISupportsWeakReference",
    ]);

    lazy.UrlbarPrefs.addObserver(this);

    this.#panelList = input.querySelector(".searchmode-switcher-panel-list");
    this.#button = input.querySelector(".searchmode-switcher");
    this.#closebutton = input.querySelector(".searchmode-switcher-close");

    // MozButton and PanelList have to be hooked up via id.
    this.#panelList.id = "searchmode-switcher-panel-list-" + input.sapName;
    this.#button.setAttribute("menuid", this.#panelList.id);

    // In XUL documents, wrap in a XUL panel to make sure it's
    // on top of the overflow panel and catches all keypresses.
    let doc = this.#panelList.ownerDocument;
    if (doc.createXULElement) {
      let panel = doc.createXULElement("panel");
      panel.setAttribute("level", "top");
      panel.setAttribute("consumeoutsideclicks", "false");
      panel.classList.add("searchmode-switcher-panel", "toolbar-menupopup");
      this.#panelList.replaceWith(panel);
      panel.appendChild(this.#panelList);
    }

    if (this.#isEnabled) {
      this.#enableObservers();
    }
  }

  #isEnabled() {
    return (
      lazy.UrlbarPrefs.get("scotchBonnet.enableOverride") ||
      this.#input.sapName == "searchbar"
    );
  }

  async #onPopupShowing() {
    // Discard event to avoid recording an abandonment.
    this.#input.controller.engagementEvent.discard();
    await this.#buildSearchModeList();
    this.#input.view.close({ showFocusBorder: false });

    if (this.#input.sapName == "urlbar") {
      Glean.urlbarUnifiedsearchbutton.opened.add(1);
    }
  }

  /**
   * Close the SearchSwitcher popup.
   */
  closePanel() {
    this.#panelList.hide(null, { force: true });
  }

  #openPreferences() {
    this.#input.window.openPreferences("paneSearch");

    if (this.#input.sapName == "urlbar") {
      Glean.urlbarUnifiedsearchbutton.picked.settings.add(1);
    }
  }

  /**
   * Exit the engine specific searchMode.
   *
   * @param {Event} event
   *        The event that triggered the searchMode exit.
   */
  exitSearchMode(event) {
    event.preventDefault();
    this.#input.searchMode = null;
    this.#selectedIndex = 0;
    this.#engines = [];
    // Update the result by the default engine.
    this.#input.startQuery();
  }

  /**
   * Called when the value of the searchMode attribute on UrlbarInput is changed.
   */
  onSearchModeChanged() {
    if (!this.#input.window || this.#input.window.closed) {
      return;
    }

    if (this.#isEnabled()) {
      this.updateSearchIcon();

      let engine = lazy.UrlbarSearchUtils.getEngineByName(
        this.#input.searchMode?.engineName
      );
      if (
        engine &&
        engine instanceof lazy.ConfigSearchEngine &&
        !engine.hasBeenUsed
      ) {
        engine.markAsUsed();
      }
    }
  }

  handleEvent(event) {
    if (event.currentTarget.localName == "panel-item") {
      this.#handlePanelItemEvent(event);
      return;
    }
    if (event.currentTarget == this.#closebutton) {
      // Prevent click and mousedown from bubbling up
      // to #button which would open the popup.
      event.stopPropagation();
      if (event.type == "click") {
        this.#input.focus();
        this.exitSearchMode(event);
      }
      return;
    }
    if (event.type == "focus") {
      this.#input.setUnifiedSearchButtonAvailability(true);
      return;
    }
    if (event.type == "showing") {
      this.#onPopupShowing();
      return;
    }
    if (event.type == "hidden") {
      if (this.#input.document.activeElement == this.#button) {
        // This moves the focus to the urlbar when the popup is closed.
        this.#input.focus();
      }
      return;
    }
    if (event.type == "keydown") {
      if (this.#input.view.isOpen) {
        // The urlbar view is open, which means the unified search button got
        // focus by tab key from urlbar.
        switch (event.keyCode) {
          case KeyEvent.DOM_VK_TAB: {
            // Move the focus to urlbar view to make cyclable.
            this.#input.focus();
            this.#input.view.selectBy(1, {
              reverse: event.shiftKey,
              userPressedTab: true,
            });
            event.preventDefault();
            return;
          }
          case KeyEvent.DOM_VK_ESCAPE: {
            this.#input.view.close();
            this.#input.focus();
            event.preventDefault();
            return;
          }
        }
      }

      // Manually open the popup on down.
      if (event.keyCode == KeyEvent.DOM_VK_DOWN) {
        this.#panelList.show(event);
      }
    }
  }

  /**
   * @param {MouseEvent|KeyboardEvent} event
   *  A mouseup, click or keydown event.
   *  Click is used for regular mouse clicks.
   *  Keydown is used for keyboard clicks (bug 1245292).
   *  Auxclick is used for middle clicks.
   */
  #handlePanelItemEvent(event) {
    switch (event.type) {
      case "click": {
        let mouseEvent = /** @type {MouseEvent} */ (event);
        // Prevent the panel from closing. We handle that manually.
        mouseEvent.stopPropagation();

        if (mouseEvent.inputSource == MouseEvent.MOZ_SOURCE_KEYBOARD) {
          // Keyboard clicks always have shiftKey=false due to bug 1245292.
          // For now, we handle them on keydown instead.
          return;
        }
        break;
      }
      case "keydown": {
        let keyboardEvent = /** @type {KeyboardEvent} */ (event);
        if (
          keyboardEvent.keyCode != KeyEvent.DOM_VK_SPACE &&
          keyboardEvent.keyCode != KeyEvent.DOM_VK_RETURN
        ) {
          return;
        }
        break;
      }
      case "auxclick": {
        let mouseEvent = /** @type {MouseEvent} */ (event);
        if (mouseEvent.button != 1) {
          // Ignore non-middle-auxclicks.
          return;
        }
        break;
      }
    }

    let panelItem = /** @type {PanelItem} */ (event.currentTarget);
    switch (panelItem.dataset.action) {
      case "openpreferences": {
        this.closePanel();
        this.#openPreferences();
        break;
      }
      case "searchmode": {
        // #remoteSearch() decides whether to close the panel or keep it open.
        let engineId = panelItem.dataset.engineId;
        this.#remoteSearch(lazy.SearchService.getEngineById(engineId), event);
        break;
      }
      case "localsearchmode": {
        this.closePanel();
        let restrict = panelItem.dataset.restrict;
        this.#localSearch(restrict);
        break;
      }
      case "installopensearch": {
        this.closePanel();
        // @ts-expect-error
        let engine = panelItem._engine;
        this.#installOpenSearchEngine(engine);
        break;
      }
    }
  }

  /**
   * @param {PanelItem} panelItem
   */
  #addCommandListeners(panelItem) {
    panelItem.addEventListener("click", this);
    panelItem.addEventListener("keydown", this);
    panelItem.addEventListener("auxclick", this);
  }

  observe(_subject, topic, data) {
    if (
      !this.#input.window ||
      this.#input.window.closed ||
      // TODO bug 2005783 stop observing when input is disconnected.
      !this.#input.isConnected
    ) {
      return;
    }

    switch (topic) {
      case "browser-search-engine-modified": {
        if (
          data === "engine-default" ||
          data === "engine-default-private" ||
          data === "engine-icon-changed"
        ) {
          this.updateSearchIcon();
        }
        break;
      }
      case "urlbar-searchmodechanged": {
        this.onSearchModeChanged();
        break;
      }
    }
  }

  /**
   * Called when a urlbar pref changes.
   *
   * @param {string} pref
   *   The name of the pref relative to `browser.urlbar`.
   */
  onPrefChanged(pref) {
    if (!this.#input.window || this.#input.window.closed) {
      return;
    }

    if (this.#input.sapName == "searchbar") {
      // The searchbar cares about neither of the two prefs.
      return;
    }

    switch (pref) {
      case "scotchBonnet.enableOverride": {
        if (lazy.UrlbarPrefs.get("scotchBonnet.enableOverride")) {
          this.#enableObservers();
          this.updateSearchIcon();
        } else {
          this.#disableObservers();
        }
        break;
      }
      case "keyword.enabled": {
        if (lazy.UrlbarPrefs.get("scotchBonnet.enableOverride")) {
          this.updateSearchIcon();
        }
        break;
      }
    }
  }

  /**
   * If the user presses Option+Up or Option+Down we open the engine list.
   *
   * @param {KeyboardEvent} event
   *   The key down event.
   */
  handleKeyDown(event) {
    if (
      (event.keyCode == KeyEvent.DOM_VK_UP ||
        event.keyCode == KeyEvent.DOM_VK_DOWN) &&
      (event.altKey || event.getModifierState("Accel"))
    ) {
      if (event.altKey) {
        this.#handleAltUpDown(event);
      } else if (event.getModifierState("Accel")) {
        this.#handleAccelUpDown(event);
      }
      event.stopPropagation();
      event.preventDefault();
      return true;
    }
    return false;
  }

  #handleAltUpDown(event) {
    this.#input.controller.focusOnUnifiedSearchButton();
    this.#panelList.show(event, this.#button);
  }

  async #handleAccelUpDown(event) {
    if (!this.#engines.length) {
      await this.#populateEngines();
    }
    this.#selectedIndex += event.keyCode == KeyEvent.DOM_VK_UP ? -1 : 1;
    if (this.#selectedIndex > this.#engines.length - 1) {
      this.#selectedIndex = 0;
    }
    if (this.#selectedIndex < 0) {
      this.#selectedIndex = this.#engines.length - 1;
    }
    let selectedEngine = this.#engines[this.#selectedIndex];
    this.#input.setSearchMode(
      {
        entry: "searchbutton",
        isPreview: false,
        source: selectedEngine?.source || lazy.UrlbarUtils.RESULT_SOURCE.SEARCH,
        engineName: selectedEngine?.name,
      },
      this.#input.window.gBrowser.selectedBrowser
    );

    let searchString = this.#getSearchString();
    if (searchString) {
      this.#input.startQuery({ allowAutofill: false });
    }
  }

  async #populateEngines() {
    let searchEngines = (await lazy.SearchService.getVisibleEngines()).filter(
      engine => !engine.hideOneOffButton
    );

    if (this.#input.sapName != "urlbar") {
      this.#engines = searchEngines;
    } else {
      // After the settings redesign we no longer use the prefs to hide local
      // search modes. Hence when the settings redesign is enabled we show
      // all local search modes regardless of the prefs.
      this.#engines = searchEngines.concat(
        lazy.UrlbarUtils.LOCAL_SEARCH_MODES.filter(engine => {
          // Do not show the search history option in PBM. tor-browser#43864.
          // Although, it can still be triggered with "^" restrict keyword or
          // through an app menu item. See also mozilla bug 1980928.
          if (
            engine.source === lazy.UrlbarUtils.RESULT_SOURCE.HISTORY &&
            lazy.PrivateBrowsingUtils.permanentPrivateBrowsing
          ) {
            return false;
          }
          return (
            lazy.settingsRedesignEnabled || lazy.UrlbarPrefs.get(engine.pref)
          );
        })
      );
    }
  }

  async updateSearchIcon() {
    let searchMode = this.#input.searchMode;

    try {
      await lazy.UrlbarSearchUtils.init();
    } catch {
      console.error("Search service failed to init");
    }

    let { label, icon } = await this.#getDisplayedEngineDetails(
      this.#input.searchMode
    );

    if (searchMode?.source != this.#input.searchMode?.source) {
      return;
    }

    const inSearchMode = this.#input.searchMode;

    if (
      this.#input.sapName != "searchbar" &&
      !lazy.UrlbarPrefs.get("keyword.enabled") &&
      !inSearchMode
    ) {
      icon = SearchModeSwitcher.ICON_GLOBE;
    }

    // If the pref is enabled, then update urlbar icons as user types.
    if (lazy.UrlbarPrefs.get("unifiedSearchButton.always")) {
      if (this.#input.focused && this.#input.value.length) {
        let result = this.#input.view?.getResultAtIndex(0);
        if (
          result &&
          (result.type == lazy.UrlbarUtils.RESULT_TYPE.URL ||
            result.type == lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH)
        ) {
          // If the user has typed a url then indicate that ENTER will visit
          // that address.
          icon = SearchModeSwitcher.ICON_GLOBE;
        }
      }
    }

    this.#button.setAttribute("iconsrc", icon);

    if (label) {
      this.#input.document.l10n.setAttributes(
        this.#button,
        "urlbar-searchmode-button3",
        { engine: label }
      );
    } else {
      this.#input.document.l10n.setAttributes(
        this.#button,
        "urlbar-searchmode-button-no-engine2"
      );
    }

    let labelEl = this.#input.querySelector(".searchmode-switcher-title");
    if (!inSearchMode) {
      labelEl.replaceChildren();
    } else {
      labelEl.textContent = label;
    }

    if (
      !lazy.UrlbarPrefs.get("keyword.enabled") &&
      this.#input.sapName != "searchbar"
    ) {
      this.#input.document.l10n.setAttributes(
        this.#button,
        "urlbar-searchmode-no-keyword2"
      );
    }
  }

  async #getSearchModeLabel(source) {
    let mode = lazy.UrlbarUtils.LOCAL_SEARCH_MODES.find(
      m => m.source == source
    );
    let [str] = await lazy.SearchModeSwitcherL10n.formatMessages([
      { id: mode.uiLabel },
    ]);
    return str.value;
  }

  async #getDisplayedEngineDetails(searchMode = null) {
    if (!lazy.SearchService.hasSuccessfullyInitialized) {
      return { label: null, icon: SearchModeSwitcher.ICON_GLASS };
    }

    if (!searchMode || searchMode.engineName) {
      let engine = searchMode
        ? lazy.UrlbarSearchUtils.getEngineByName(searchMode.engineName)
        : lazy.UrlbarSearchUtils.getDefaultEngine(
            lazy.PrivateBrowsingUtils.isWindowPrivate(this.#input.window)
          );
      if (!engine) {
        return { label: null, icon: SearchModeSwitcher.ICON_GLASS };
      }
      let icon = (await engine.getIconURL()) ?? SearchModeSwitcher.ICON_GLASS;
      return { label: engine.name, icon };
    }

    let mode = lazy.UrlbarUtils.LOCAL_SEARCH_MODES.find(
      m => m.source == searchMode.source
    );
    return {
      label: await this.#getSearchModeLabel(searchMode.source),
      icon: mode.icon,
    };
  }

  /**
   * Builds the popup and dispatches a rebuild event on the popup when finished.
   */
  async #buildSearchModeList() {
    for (let item of this.#panelList.querySelectorAll("panel-item")) {
      item.remove();
    }

    let browser = this.#input.window.gBrowser;
    let installedEngineSeparator = this.#panelList.querySelector(
      ".searchmode-switcher-panel-installed-engine-separator"
    );
    let footerSeparator = this.#panelList.querySelector(
      ".searchmode-switcher-panel-footer-separator"
    );

    await this.#populateEngines();
    for (let engine of this.#engines) {
      if (engine.source) {
        footerSeparator.before(await this.#buildLocalSearchButton(engine));
      } else if (engine.name) {
        let menuitem = await this.#buildEngineSearchButton(engine);
        installedEngineSeparator.before(menuitem);
      }
    }
    this.#buildSettingsButton();

    // Add engines that can be installed.
    let openSearchEngines = lazy.OpenSearchManager.getEngines(
      browser.selectedBrowser
    );
    openSearchEngines = openSearchEngines.slice(
      0,
      SearchModeSwitcher.MAX_OPENSEARCH_ENGINES
    );

    for (let engine of openSearchEngines) {
      let menuitem = this.#createButton(engine.icon);
      this.#input.document.l10n.setAttributes(
        menuitem,
        "urlbar-searchmode-popup-add-engine",
        {
          engineName: engine.title,
        }
      );
      menuitem.classList.add("searchmode-switcher-addEngine");
      menuitem.dataset.action = "installopensearch";
      // This attribute is for testing.
      menuitem.dataset.engineName = engine.title;
      this.#addCommandListeners(menuitem);
      // @ts-expect-error
      menuitem._engine = engine;

      footerSeparator.after(menuitem);
    }

    if (this.#panelList.wasOpenedByKeyboard) {
      // Focus will not be on first item anymore because new
      // items were added after the panel list was shown.
      this.#panelList.focusWalker.currentNode = this.#panelList;
      this.#panelList.focusWalker.nextNode();
    }

    // Hide footer separator if there are no menuitems between both separators.
    footerSeparator.toggleAttribute(
      "hidden",
      footerSeparator.previousElementSibling == installedEngineSeparator
    );

    this.#panelList.dispatchEvent(new Event("rebuild"));
  }

  /**
   * @param {MouseEvent|KeyboardEvent} event
   * @returns {string}
   *   Where the search engine result page should be opened.
   */
  #whereToOpenSerp(event) {
    let where = lazy.BrowserUtils.whereToOpenLink(event, false, true);
    // Usually, shift means "open in new window", but in the search
    // mode switcher it means "open SERP even if urlbar is empty",
    // so we just return tab, tabshifted or current but never window.
    if (where.startsWith("tab")) {
      return where;
    }
    return "current";
  }

  /**
   * Ideally the settings button would be in the markup because it never
   * changes but that causes an an assertion error in BindingUtils.cpp.
   */
  #buildSettingsButton() {
    // Icon is set via css based on the class.
    let menuitem = this.#createButton(undefined);
    menuitem.classList.add("searchmode-switcher-panel-search-settings-button");
    menuitem.dataset.action = "openpreferences";
    menuitem.setAttribute("data-l10n-attrs", "accesskey");
    this.#input.document.l10n.setAttributes(
      menuitem,
      Services.prefs.getBoolPref("browser.nova.enabled", false)
        ? "urlbar-searchmode-popup-settings"
        : "urlbar-searchmode-popup-search-settings"
    );
    this.#addCommandListeners(menuitem);
    this.#panelList.appendChild(menuitem);
  }

  async #buildEngineSearchButton(engine) {
    let icon = await engine.getIconURL();
    let menuitem = this.#createButton(icon, engine.name);
    menuitem.classList.add("searchmode-switcher-installed");
    menuitem.setAttribute("label", engine.name);
    menuitem.setAttribute("title", engine.name);
    menuitem.setAttribute("accesskey", engine.name[0]);
    menuitem.setAttribute("closemenu", "none");

    if (engine.isNew() && engine instanceof lazy.AppProvidedConfigEngine) {
      menuitem.setAttribute("badge-type", "new");
    }

    menuitem.dataset.engineId = engine.id;
    // This attribute is for testing.
    menuitem.dataset.engineName = engine.name;
    menuitem.dataset.action = "searchmode";
    this.#addCommandListeners(menuitem);
    return menuitem;
  }

  /**
   * @param {LocalSearchMode} mode
   * @returns {Promise<PanelItem>}
   */
  async #buildLocalSearchButton(mode) {
    let sourceName = lazy.UrlbarUtils.getResultSourceName(mode.source);
    let { icon } = await this.#getDisplayedEngineDetails(mode);
    let menuitem = this.#createButton(icon);
    menuitem.classList.add(
      "searchmode-switcher-local",
      `search-button-${sourceName}`
    );
    menuitem.dataset.action = "localsearchmode";
    menuitem.dataset.restrict = mode.restrict;
    menuitem.setAttribute("data-l10n-attrs", "accesskey");
    this.#addCommandListeners(menuitem);
    this.#input.document.l10n.setAttributes(menuitem, mode.uiLabel);
    return menuitem;
  }

  /**
   * Enables a local search mode based on the restrict token.
   *
   * @param {string} restrict
   *   The restrict token
   */
  #localSearch(restrict) {
    this.#input.search(restrict + " " + this.#getSearchString(), {
      searchModeEntry: "searchbutton",
    });

    if (this.#input.sapName == "urlbar") {
      Glean.urlbarUnifiedsearchbutton.picked.local_search.add(1);
    }
  }

  /**
   * Enters searchmode in the urlbar or opens a SERP, depending
   * on whether the urlbar is empty.
   * Shift can be used to force the SERP.
   * Also handles closing the panel.
   *
   * @param {SearchEngine} searchEngine
   *   The engine to search with.
   * @param {KeyboardEvent|MouseEvent} event
   *   The event that triggered the search.
   */
  #remoteSearch(searchEngine, event) {
    let whereToOpenSerp = this.#whereToOpenSerp(event);
    let searchString = this.#getSearchString();
    if (!event.shiftKey && whereToOpenSerp == "current") {
      // Go into searchmode.
      this.closePanel();
      this.#input.search(searchString, {
        searchEngine,
        searchModeEntry: "searchbutton",
      });
    } else {
      // Go directly to SERP.
      if (whereToOpenSerp == "current") {
        this.closePanel();
      }

      this.#input.openSearchEnginePage(searchString, {
        event,
        searchEngine,
        where: whereToOpenSerp,
        inBackground: true,
      });
    }

    if (this.#input.sapName == "urlbar") {
      // TODO do we really need to distinguish here?
      Glean.urlbarUnifiedsearchbutton.picked[
        searchEngine instanceof lazy.ConfigSearchEngine
          ? "builtin_search"
          : "addon_search"
      ].add(1);
    }
  }

  /**
   * The string to use when starting a search via the search mode switcher.
   *
   * @returns {string}
   */
  #getSearchString() {
    if (this.#input.getAttribute("pageproxystate") == "valid") {
      return "";
    }
    return this.#input.value;
  }

  /**
   * Returns whether the event's target is an item
   * in the search mode switcher popup.
   *
   * @param {Event|null|undefined} event
   * @returns {boolean}
   */
  eventTargetIsPanelItem(event) {
    let target = event?.target;
    if (!target || !("classList" in target)) {
      return false;
    }
    let classList = /** @type {DOMTokenList}*/ (target.classList);

    return (
      classList.contains("searchmode-switcher-addEngine") ||
      classList.contains("searchmode-switcher-installed") ||
      classList.contains("searchmode-switcher-local")
    );
  }

  #enableObservers() {
    Services.obs.addObserver(this, "browser-search-engine-modified", true);
    Services.obs.addObserver(this, "urlbar-searchmodechanged", true);

    this.#button.addEventListener("focus", this);
    this.#button.addEventListener("keydown", this);

    this.#panelList.addEventListener("showing", this);
    this.#panelList.addEventListener("hidden", this);

    this.#closebutton.addEventListener("click", this);
    this.#closebutton.addEventListener("mousedown", this);
  }

  #disableObservers() {
    Services.obs.removeObserver(this, "browser-search-engine-modified");
    Services.obs.removeObserver(this, "urlbar-searchmodechanged");

    this.#button.removeEventListener("focus", this);
    this.#button.removeEventListener("keydown", this);

    this.#panelList.removeEventListener("showing", this);
    this.#panelList.removeEventListener("hidden", this);

    this.#closebutton.removeEventListener("click", this);
    this.#closebutton.removeEventListener("mousedown", this);
  }

  /**
   * @param {string|undefined} icon
   *   The icon. Pass undefined to use the default engine icon.
   * @param {string} [label]
   *   The label. Can be omitted when setting it via fluent.
   */
  #createButton(icon, label) {
    let panelitem = /**@type {PanelItem} */ (
      this.#input.document.createElementNS(
        "http://www.w3.org/1999/xhtml",
        "panel-item"
      )
    );
    if (label) {
      panelitem.textContent = label;
    }
    panelitem.style.setProperty(
      "--icon-url",
      `url(${icon ?? DEFAULT_ENGINE_ICON})`
    );

    return panelitem;
  }

  /**
   * Installs open search engine and enters search mode.
   *
   * @param {OpenSearchData} engine
   *   The engine to install.
   */
  async #installOpenSearchEngine(engine) {
    let topic = "browser-search-engine-modified";
    /** @type {(subject: {wrappedJSObject: SearchEngine}) => void} */
    let observer = subject => {
      Services.obs.removeObserver(observer, topic);
      this.#input.search(this.#getSearchString(), {
        searchEngine: subject.wrappedJSObject,
        searchModeEntry: "searchbutton",
      });
    };
    Services.obs.addObserver(observer, topic);
    if (this.#input.sapName == "urlbar") {
      Glean.urlbarUnifiedsearchbutton.picked.addon_search.add(1);
    }

    await lazy.SearchUIUtils.addOpenSearchEngine(
      engine.uri,
      engine.icon,
      this.#input.window.gBrowser.selectedBrowser.browsingContext
    );
  }
}
