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

"use strict";

// Wrap in a block to prevent leaking to window scope.
{
  ChromeUtils.defineESModuleGetters(this, {
    BrowserSearchTelemetry:
      "moz-src:///browser/components/search/BrowserSearchTelemetry.sys.mjs",
    BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
    SearchOneOffs: "moz-src:///browser/components/search/SearchOneOffs.sys.mjs",
  });

  /**
   * A richlistbox popup custom element for for a browser search autocomplete
   * widget.
   */
  class MozSearchAutocompleteRichlistboxPopup extends MozElements.MozAutocompleteRichlistboxPopup {
    constructor() {
      super();

      this.addEventListener("popupshowing", () => {
        // First handle deciding if we are showing the reduced version of the
        // popup containing only the preferences button. We do this if the
        // glass icon has been clicked if the text field is empty.
        if (this.searchbar.hasAttribute("showonlysettings")) {
          this.searchbar.removeAttribute("showonlysettings");
          this.setAttribute("showonlysettings", "true");

          // Setting this with an xbl-inherited attribute gets overridden the
          // second time the user clicks the glass icon for some reason...
          this.richlistbox.collapsed = true;
        } else {
          this.removeAttribute("showonlysettings");
          // Uncollapse as long as we have a view which has >= 1 row.
          // The autocomplete binding itself will take care of uncollapsing later,
          // if we currently have no rows but end up having some in the future
          // when the search string changes
          this.richlistbox.collapsed = this.matchCount == 0;
        }

        // Show the current default engine in the top header of the panel.
        this.updateHeader().catch(console.error);

        this._oneOffButtons.addEventListener(
          "SelectedOneOffButtonChanged",
          this
        );
      });

      this.addEventListener("popuphiding", () => {
        this._oneOffButtons.removeEventListener(
          "SelectedOneOffButtonChanged",
          this
        );
      });

      /**
       * This handles clicks on the topmost "Foo Search" header in the
       * popup (hbox.search-panel-header]).
       */
      this.addEventListener("click", event => {
        if (event.button == 2) {
          // Ignore right clicks.
          return;
        }
        let button = event.originalTarget;
        let engine = button.parentNode.engine;
        if (!engine) {
          return;
        }
        if (this.searchbar.value) {
          this.oneOffButtons.handleSearchCommand(event, engine);
        } else if (event.shiftKey) {
          this.openSearchForm(event, engine);
        }
      });

      this._bundle = null;
    }

    static get inheritedAttributes() {
      return {
        ".search-panel-current-engine": "showonlysettings",
        ".searchbar-engine-image": "src",
      };
    }

    // We override this because even though we have a shadow root, we want our
    // inheritance to be done on the light tree.
    getElementForAttrInheritance(selector) {
      return this.querySelector(selector);
    }

    initialize() {
      super.initialize();
      this.initializeAttributeInheritance();

      this._searchOneOffsContainer = this.querySelector(".search-one-offs");
      this._searchbarEngine = this.querySelector(".search-panel-header");
      this._searchbarEngineName = this.querySelector(".searchbar-engine-name");
      this._oneOffButtons = new SearchOneOffs(this._searchOneOffsContainer);
      this._searchbar = document.getElementById("searchbar");
    }

    get oneOffButtons() {
      if (!this._oneOffButtons) {
        this.initialize();
      }
      return this._oneOffButtons;
    }

    static get markup() {
      return `
      <hbox class="search-panel-header search-panel-current-engine">
        <image class="searchbar-engine-image"/>
        <label class="searchbar-engine-name" flex="1" crop="end" role="presentation"/>
      </hbox>
      <menuseparator class="searchbar-separator"/>
      <richlistbox class="autocomplete-richlistbox search-panel-tree"/>
      <menuseparator class="searchbar-separator"/>
      <hbox class="search-one-offs" is_searchbar="true"/>
    `;
    }

    get searchOneOffsContainer() {
      if (!this._searchOneOffsContainer) {
        this.initialize();
      }
      return this._searchOneOffsContainer;
    }

    get searchbarEngine() {
      if (!this._searchbarEngine) {
        this.initialize();
      }
      return this._searchbarEngine;
    }

    get searchbarEngineName() {
      if (!this._searchbarEngineName) {
        this.initialize();
      }
      return this._searchbarEngineName;
    }

    get searchbar() {
      if (!this._searchbar) {
        this.initialize();
      }
      return this._searchbar;
    }

    get bundle() {
      if (!this._bundle) {
        const kBundleURI = "chrome://browser/locale/search.properties";
        this._bundle = Services.strings.createBundle(kBundleURI);
      }
      return this._bundle;
    }

    openAutocompletePopup(aInput, aElement) {
      // initially the panel is hidden
      // to avoid impacting startup / new window performance
      aInput.popup.hidden = false;

      // this method is defined on the base binding
      this._openAutocompletePopup(aInput, aElement);
    }

    onPopupClick(aEvent) {
      // Ignore all right-clicks
      if (aEvent.button == 2) {
        return;
      }

      this.searchbar.telemetrySelectedIndex = this.selectedIndex;

      // Check for unmodified left-click, and use default behavior
      if (
        aEvent.button == 0 &&
        !aEvent.shiftKey &&
        !aEvent.ctrlKey &&
        !aEvent.altKey &&
        !aEvent.metaKey
      ) {
        this.input.controller.handleEnter(true, aEvent);
        return;
      }

      // Check for middle-click or modified clicks on the search bar
      BrowserSearchTelemetry.recordSearchSuggestionSelectionMethod(
        aEvent,
        this.selectedIndex
      );

      // Handle search bar popup clicks
      let search = this.input.controller.getValueAt(this.selectedIndex);

      // open the search results according to the clicking subtlety
      let where = BrowserUtils.whereToOpenLink(aEvent, false, true);
      let params = {};

      // But open ctrl/cmd clicks on autocomplete items in a new background tab.
      let modifier =
        AppConstants.platform == "macosx" ? aEvent.metaKey : aEvent.ctrlKey;
      if (
        where == "tab" &&
        MouseEvent.isInstance(aEvent) &&
        (aEvent.button == 1 || modifier)
      ) {
        params.inBackground = true;
      }

      // leave the popup open for background tab loads
      if (!(where == "tab" && params.inBackground)) {
        // close the autocomplete popup and revert the entered search term
        this.closePopup();
        this.input.controller.handleEscape();
      }

      this.searchbar.doSearch(search, where, null, params);
      if (where == "tab" && params.inBackground) {
        this.searchbar.focus();
      } else {
        this.searchbar.value = search;
      }
    }

    /**
     * @type {string}
     *   The current engine name being displayed in updateHeader.
     */
    #currentEngineName;

    /**
     * Updates the header of the pop-up with the search engine name and icon.
     *
     * @param {nsISearchEngine} [engine]
     *   The engine to use, if not specified falls back to the default engine.
     */
    async updateHeader(engine) {
      if (!engine) {
        if (PrivateBrowsingUtils.isWindowPrivate(window)) {
          engine = await Services.search.getDefaultPrivate();
        } else {
          engine = await Services.search.getDefault();
        }
      }
      this.#currentEngineName = engine.name;

      let uri = await engine.getIconURL();

      // If the engine name has changed since we started loading, this means
      // that getIconURL probably took a long time and we had an update in
      // the meantime. Hence we skip updating to avoid displaying the wrong
      // thing.
      if (engine.name != this.#currentEngineName) {
        return;
      }

      if (uri) {
        this.setAttribute("src", uri);
      } else {
        // If the default has just been changed to a provider without icon,
        // avoid showing the icon of the previous default provider.
        this.removeAttribute("src");
      }

      let headerText = this.bundle.formatStringFromName("searchHeader", [
        engine.name,
      ]);
      this.searchbarEngineName.setAttribute("value", headerText);
      this.searchbarEngine.engine = engine;
    }

    /**
     * This is called when a one-off is clicked and when "search in new tab"
     * is selected from a one-off context menu.
     *
     * @param {Event} event
     *   The event that triggered the search.
     * @param {nsISearchEngine} engine
     *   The search engine being used for the search.
     * @param {string} where
     *   Where the search should be opened (current tab, new tab, window etc).
     * @param {object} params
     *   The parameters associated with opening the search.
     */
    handleOneOffSearch(event, engine, where, params) {
      this.searchbar.handleSearchCommandWhere(event, engine, where, params);
    }

    openSearchForm(event, engine, forceNewTab = false) {
      let { where, params } = this.oneOffButtons._whereToOpen(
        event,
        forceNewTab
      );
      this.searchbar.openSearchFormWhere(event, engine, where, params);
    }

    /**
     * Passes DOM events for the popup to the _on_<event type> methods.
     *
     * @param {Event} event
     *   DOM event from the <popup>.
     */
    handleEvent(event) {
      let methodName = "_on_" + event.type;
      if (methodName in this) {
        this[methodName](event);
      } else {
        throw new Error("Unrecognized UrlbarView event: " + event.type);
      }
    }
    _on_SelectedOneOffButtonChanged() {
      let engine =
        this.oneOffButtons.selectedButton &&
        this.oneOffButtons.selectedButton.engine;
      this.updateHeader(engine).catch(console.error);
    }
  }

  customElements.define(
    "search-autocomplete-richlistbox-popup",
    MozSearchAutocompleteRichlistboxPopup,
    {
      extends: "panel",
    }
  );
}
