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

/* eslint-env mozilla/browser-window */

/* eslint-disable jsdoc/valid-types */
/**
 * @typedef {import("../../../../toolkit/components/translations/translations").LangTags} LangTags
 */
/* eslint-enable jsdoc/valid-types */

ChromeUtils.defineESModuleGetters(this, {
  TranslationsTelemetry:
    "chrome://browser/content/translations/TranslationsTelemetry.sys.mjs",
});

/**
 * The set of actions that can occur from interaction with the
 * translations panel.
 */
const PageAction = Object.freeze({
  NO_CHANGE: "NO_CHANGE",
  RESTORE_PAGE: "RESTORE_PAGE",
  TRANSLATE_PAGE: "TRANSLATE_PAGE",
});

/**
 * A mechanism for determining the next relevant page action
 * based on the current translated state of the page and the state
 * of the persistent options in the translations panel settings.
 */
class CheckboxPageAction {
  /**
   * Whether or not translations is active on the page.
   *
   * @type {boolean}
   */
  #translationsActive = false;

  /**
   * Whether the always-translate-language menuitem is checked
   * in the translations panel settings menu.
   *
   * @type {boolean}
   */
  #alwaysTranslateLanguage = false;

  /**
   * Whether the never-translate-language menuitem is checked
   * in the translations panel settings menu.
   *
   * @type {boolean}
   */
  #neverTranslateLanguage = false;

  /**
   * Whether the never-translate-site menuitem is checked
   * in the translations panel settings menu.
   *
   * @type {boolean}
   */
  #neverTranslateSite = false;

  /**
   * @param {boolean} translationsActive
   * @param {boolean} alwaysTranslateLanguage
   * @param {boolean} neverTranslateLanguage
   * @param {boolean} neverTranslateSite
   */
  constructor(
    translationsActive,
    alwaysTranslateLanguage,
    neverTranslateLanguage,
    neverTranslateSite
  ) {
    this.#translationsActive = translationsActive;
    this.#alwaysTranslateLanguage = alwaysTranslateLanguage;
    this.#neverTranslateLanguage = neverTranslateLanguage;
    this.#neverTranslateSite = neverTranslateSite;
  }

  /**
   * Accepts four integers that are either 0 or 1 and returns
   * a single, unique number for each possible combination of
   * values.
   *
   * @param {number} translationsActive
   * @param {number} alwaysTranslateLanguage
   * @param {number} neverTranslateLanguage
   * @param {number} neverTranslateSite
   *
   * @returns {number} - An integer representation of the state
   */
  static #computeState(
    translationsActive,
    alwaysTranslateLanguage,
    neverTranslateLanguage,
    neverTranslateSite
  ) {
    return (
      (translationsActive << 3) |
      (alwaysTranslateLanguage << 2) |
      (neverTranslateLanguage << 1) |
      neverTranslateSite
    );
  }

  /**
   * Returns the current state of the data members as a single number.
   *
   * @returns {number} - An integer representation of the state
   */
  #state() {
    return CheckboxPageAction.#computeState(
      Number(this.#translationsActive),
      Number(this.#alwaysTranslateLanguage),
      Number(this.#neverTranslateLanguage),
      Number(this.#neverTranslateSite)
    );
  }

  /**
   * Returns the next page action to take when the always-translate-language
   * menuitem is toggled in the translations panel settings menu.
   *
   * @returns {PageAction}
   */
  alwaysTranslateLanguage() {
    switch (this.#state()) {
      case CheckboxPageAction.#computeState(1, 1, 0, 1):
      case CheckboxPageAction.#computeState(1, 1, 0, 0):
        return PageAction.RESTORE_PAGE;
      case CheckboxPageAction.#computeState(0, 0, 1, 0):
      case CheckboxPageAction.#computeState(0, 0, 0, 0):
        return PageAction.TRANSLATE_PAGE;
    }
    return PageAction.NO_CHANGE;
  }

  /**
   * Returns the next page action to take when the never-translate-language
   * menuitem is toggled in the translations panel settings menu.
   *
   * @returns {PageAction}
   */
  neverTranslateLanguage() {
    switch (this.#state()) {
      case CheckboxPageAction.#computeState(1, 1, 0, 1):
      case CheckboxPageAction.#computeState(1, 1, 0, 0):
      case CheckboxPageAction.#computeState(1, 0, 0, 1):
      case CheckboxPageAction.#computeState(1, 0, 0, 0):
        return PageAction.RESTORE_PAGE;
    }
    return PageAction.NO_CHANGE;
  }

  /**
   * Returns the next page action to take when the never-translate-site
   * menuitem is toggled in the translations panel settings menu.
   *
   * @returns {PageAction}
   */
  neverTranslateSite() {
    switch (this.#state()) {
      case CheckboxPageAction.#computeState(1, 1, 0, 0):
      case CheckboxPageAction.#computeState(1, 0, 1, 0):
      case CheckboxPageAction.#computeState(1, 0, 0, 0):
        return PageAction.RESTORE_PAGE;
      case CheckboxPageAction.#computeState(0, 1, 0, 1):
        return PageAction.TRANSLATE_PAGE;
    }
    return PageAction.NO_CHANGE;
  }
}

/**
 * This singleton class controls the Translations popup panel.
 *
 * This component is a `/browser` component, and the actor is a `/toolkit` actor, so care
 * must be taken to keep the presentation (this component) from the state management
 * (the Translations actor). This class reacts to state changes coming from the
 * Translations actor.
 */
var TranslationsPanel = new (class {
  /** @type {Console?} */
  #console;

  /**
   * The cached detected languages for both the document and the user.
   *
   * @type {null | LangTags}
   */
  detectedLanguages = null;

  /**
   * Lazily get a console instance.
   *
   * @returns {Console}
   */
  get console() {
    if (!this.#console) {
      this.#console = console.createInstance({
        maxLogLevelPref: "browser.translations.logLevel",
        prefix: "Translations",
      });
    }
    return this.#console;
  }

  /**
   * Where the lazy elements are stored.
   *
   * @type {Record<string, Element>?}
   */
  #lazyElements;

  /**
   * Lazily creates the dom elements, and lazily selects them.
   *
   * @returns {Record<string, Element>}
   */
  get elements() {
    if (!this.#lazyElements) {
      // Lazily turn the template into a DOM element.
      /** @type {HTMLTemplateElement} */
      const wrapper = document.getElementById("template-translations-panel");
      const panel = wrapper.content.firstElementChild;
      wrapper.replaceWith(wrapper.content);

      const settingsButton = document.getElementById(
        "translations-panel-settings"
      );
      // Clone the settings toolbarbutton across all the views.
      for (const header of panel.querySelectorAll(".panel-header")) {
        if (header.contains(settingsButton)) {
          continue;
        }
        const settingsButtonClone = settingsButton.cloneNode(true);
        settingsButtonClone.removeAttribute("id");
        header.appendChild(settingsButtonClone);
      }

      // Lazily select the elements.
      this.#lazyElements = {
        panel,
        settingsButton,
        // The rest of the elements are set by the getter below.
      };

      /**
       * Define a getter on #lazyElements that gets the element by an id
       * or class name.
       */
      const getter = (name, discriminator) => {
        let element;
        Object.defineProperty(this.#lazyElements, name, {
          get: () => {
            if (!element) {
              if (discriminator[0] === ".") {
                // Lookup by class
                element = document.querySelector(discriminator);
              } else {
                // Lookup by id
                element = document.getElementById(discriminator);
              }
            }
            if (!element) {
              throw new Error(
                `Could not find "${name}" at "#${discriminator}".`
              );
            }
            return element;
          },
        });
      };

      // Getters by id
      getter("appMenuButton", "PanelUI-menu-button");
      getter("button", "translations-button");
      getter("buttonLocale", "translations-button-locale");
      getter("buttonCircleArrows", "translations-button-circle-arrows");
      getter("defaultTranslate", "translations-panel-translate");
      getter("error", "translations-panel-error");
      getter("errorMessage", "translations-panel-error-message");
      getter("errorMessageHint", "translations-panel-error-message-hint");
      getter("errorHintAction", "translations-panel-translate-hint-action");
      getter("fromMenuList", "translations-panel-from");
      getter("header", "translations-panel-header");
      getter("langSelection", "translations-panel-lang-selection");
      getter("multiview", "translations-panel-multiview");
      getter("cancelButton", "translations-panel-cancel");
      getter("restoreButton", "translations-panel-restore-button");
      getter("toMenuList", "translations-panel-to");
      getter("unsupportedHint", "translations-panel-error-unsupported-hint");

      // Getters by class
      getter(
        "alwaysTranslateLanguageMenuItem",
        ".always-translate-language-menuitem"
      );
      getter(
        "neverTranslateLanguageMenuItem",
        ".never-translate-language-menuitem"
      );
      getter("neverTranslateSiteMenuItem", ".never-translate-site-menuitem");
    }

    return this.#lazyElements;
  }

  /**
   * Cache the last command used for error hints so that it can be later removed.
   */
  #lastHintCommand = null;

  /**
   * @param {object} options
   * @param {string} options.message - l10n id
   * @param {string} options.hint - l10n id
   * @param {string} options.actionText - l10n id
   * @param {Function} options.actionCommand - The action to perform.
   */
  #showError({
    message,
    hint,
    actionText: hintCommandText,
    actionCommand: hintCommand,
  }) {
    const { error, errorMessage, errorMessageHint, errorHintAction } =
      this.elements;
    error.hidden = false;
    document.l10n.setAttributes(errorMessage, message);

    if (hint) {
      errorMessageHint.hidden = false;
      document.l10n.setAttributes(errorMessageHint, hint);
    } else {
      errorMessageHint.hidden = true;
    }

    if (hintCommand && hintCommandText) {
      errorHintAction.removeEventListener("command", this.#lastHintCommand);
      this.#lastHintCommand = hintCommand;
      errorHintAction.addEventListener("command", hintCommand);
      errorHintAction.hidden = false;
      document.l10n.setAttributes(errorHintAction, hintCommandText);
    } else {
      errorHintAction.hidden = true;
    }
  }

  /**
   * @returns {TranslationsParent}
   */
  #getTranslationsActor() {
    const actor =
      gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(
        "Translations"
      );

    if (!actor) {
      throw new Error("Unable to get the TranslationsParent");
    }
    return actor;
  }

  /**
   * Fetches the language tags for the document and the user and caches the results
   * Use `#getCachedDetectedLanguages` when the lang tags do not need to be re-fetched.
   * This requires a bit of work to do, so prefer the cached version when possible.
   *
   * @returns {Promise<LangTags>}
   */
  async #fetchDetectedLanguages() {
    this.detectedLanguages =
      await this.#getTranslationsActor().getDetectedLanguages();
    return this.detectedLanguages;
  }

  /**
   * If the detected language tags have been retrieved previously, return the cached
   * version. Otherwise do a fresh lookup of the document's language tag.
   *
   * @returns {Promise<LangTags>}
   */
  async #getCachedDetectedLanguages() {
    if (!this.detectedLanguages) {
      return this.#fetchDetectedLanguages();
    }
    return this.detectedLanguages;
  }

  /**
   * @type {"initialized" | "error" | "uninitialized"}
   */
  #langListsPhase = "uninitialized";

  /**
   * Builds the <menulist> of languages for both the "from" and "to". This can be
   * called every time the popup is shown, as it will retry when there is an error
   * (such as a network error) or be a noop if it's already initialized.
   *
   * TODO(Bug 1813796) This needs to be updated when the supported languages change
   * via RemoteSettings.
   */
  async #ensureLangListsBuilt() {
    switch (this.#langListsPhase) {
      case "initialized":
        // This has already been initialized.
        return;
      case "error":
        // Attempt to re-initialize.
        this.#langListsPhase = "uninitialized";
        break;
      case "uninitialized":
        // Ready to initialize.
        break;
      default:
        this.console.error("Unknown langList phase", this.#langListsPhase);
    }

    try {
      /** @type {SupportedLanguages} */
      const { languagePairs, fromLanguages, toLanguages } =
        await this.#getTranslationsActor().getSupportedLanguages();

      // Verify that we are in a proper state.
      if (languagePairs.length === 0) {
        throw new Error("No translation languages were retrieved.");
      }

      const { panel } = this.elements;
      const fromPopups = panel.querySelectorAll(
        ".translations-panel-language-menupopup-from"
      );
      const toPopups = panel.querySelectorAll(
        ".translations-panel-language-menupopup-to"
      );

      for (const popup of fromPopups) {
        for (const { langTag, isBeta, displayName } of fromLanguages) {
          const fromMenuItem = document.createXULElement("menuitem");
          fromMenuItem.setAttribute("value", langTag);
          if (isBeta) {
            document.l10n.setAttributes(
              fromMenuItem,
              "translations-panel-displayname-beta",
              { language: displayName }
            );
          } else {
            fromMenuItem.setAttribute("label", displayName);
          }
          popup.appendChild(fromMenuItem);
        }
      }

      for (const popup of toPopups) {
        for (const { langTag, isBeta, displayName } of toLanguages) {
          const toMenuItem = document.createXULElement("menuitem");
          toMenuItem.setAttribute("value", langTag);
          if (isBeta) {
            document.l10n.setAttributes(
              toMenuItem,
              "translations-panel-displayname-beta",
              { language: displayName }
            );
          } else {
            toMenuItem.setAttribute("label", displayName);
          }
          popup.appendChild(toMenuItem);
        }
      }

      this.#langListsPhase = "initialized";
    } catch (error) {
      this.console.error(error);
      this.#langListsPhase = "error";
    }
  }

  /**
   * Reactively sets the views based on the async state changes of the engine, and
   * other component state changes.
   */
  #updateViewFromTranslationStatus() {
    const {
      defaultTranslate,
      toMenuList,
      fromMenuList,
      header,
      cancelButton,
      restoreButton,
    } = this.elements;
    const { requestedTranslationPair, isEngineReady } =
      this.#getTranslationsActor().languageState;

    if (
      requestedTranslationPair &&
      !isEngineReady &&
      toMenuList.value === requestedTranslationPair.toLanguage &&
      fromMenuList.value === requestedTranslationPair.fromLanguage
    ) {
      // A translation has been requested, but is not ready yet.
      document.l10n.setAttributes(
        defaultTranslate,
        "translations-panel-translate-button-loading"
      );
      defaultTranslate.disabled = true;
      cancelButton.hidden = false;
      restoreButton.hidden = true;
    } else {
      document.l10n.setAttributes(
        defaultTranslate,
        "translations-panel-translate-button"
      );
      defaultTranslate.disabled =
        // The translation languages are the same, don't allow this translation.
        toMenuList.value === fromMenuList.value ||
        // No "to" language was provided.
        !toMenuList.value ||
        // No "from" language was provided.
        !fromMenuList.value ||
        // This is the requested translation pair.
        (requestedTranslationPair &&
          requestedTranslationPair.fromLanguage === fromMenuList.value &&
          requestedTranslationPair.toLanguage === toMenuList.value);
    }

    if (requestedTranslationPair && isEngineReady) {
      const { fromLanguage, toLanguage } = requestedTranslationPair;
      const displayNames = new Services.intl.DisplayNames(undefined, {
        type: "language",
      });
      cancelButton.hidden = true;
      restoreButton.hidden = false;

      document.l10n.setAttributes(header, "translations-panel-revisit-header", {
        fromLanguage: displayNames.of(fromLanguage),
        toLanguage: displayNames.of(toLanguage),
      });
    } else {
      document.l10n.setAttributes(header, "translations-panel-header");
    }
  }

  /**
   * Show the default view of choosing a source and target language.
   *
   * @param {boolean} force - Force the page to show translation options.
   */
  async #showDefaultView(force = false) {
    const {
      fromMenuList,
      multiview,
      panel,
      error,
      toMenuList,
      defaultTranslate,
      langSelection,
    } = this.elements;

    this.#updateViewFromTranslationStatus();

    if (this.#langListsPhase === "error") {
      // There was an error, display it in the view rather than the language
      // dropdowns.
      const { restoreButton, cancelButton, errorHintAction } = this.elements;

      this.#showError({
        message: "translations-panel-error-load-languages",
        hint: "translations-panel-error-load-languages-hint",
        actionText: "translations-panel-error-load-languages-hint-button",
        actionCommand: () => this.#reloadLangList(),
      });

      defaultTranslate.disabled = true;
      restoreButton.hidden = true;
      cancelButton.hidden = false;
      langSelection.hidden = true;
      errorHintAction.disabled = false;
      return;
    }

    // Remove any old selected values synchronously before asking for new ones.
    fromMenuList.value = "";
    error.hidden = true;
    langSelection.hidden = false;

    /** @type {null | LangTags} */
    const langTags = await this.#fetchDetectedLanguages();
    if (langTags?.isDocLangTagSupported || force) {
      // Show the default view with the language selection
      const { restoreButton, cancelButton } = this.elements;

      if (langTags?.isDocLangTagSupported) {
        fromMenuList.value = langTags?.docLangTag ?? "";
      } else {
        fromMenuList.value = "";
      }
      toMenuList.value = langTags?.userLangTag ?? "";

      this.onChangeLanguages();

      restoreButton.hidden = true;
      cancelButton.hidden = false;
      multiview.setAttribute("mainViewId", "translations-panel-view-default");
    } else {
      // Show the "unsupported language" view.
      const { unsupportedHint } = this.elements;
      multiview.setAttribute(
        "mainViewId",
        "translations-panel-view-unsupported-language"
      );
      let language;
      if (langTags?.docLangTag) {
        const displayNames = new Intl.DisplayNames(undefined, {
          type: "language",
          fallback: "none",
        });
        language = displayNames.of(langTags.docLangTag);
      }
      if (language) {
        document.l10n.setAttributes(
          unsupportedHint,
          "translations-panel-error-unsupported-hint-known",
          { language }
        );
      } else {
        document.l10n.setAttributes(
          unsupportedHint,
          "translations-panel-error-unsupported-hint-unknown"
        );
      }
    }

    // Focus the "from" language, as it is the only field not set.
    panel.addEventListener(
      "ViewShown",
      () => {
        if (!fromMenuList.value) {
          fromMenuList.focus();
        }
        if (!toMenuList.value) {
          toMenuList.focus();
        }
      },
      { once: true }
    );
  }

  /**
   * Updates the checked states of the settings menu checkboxes that
   * pertain to languages.
   */
  async #updateSettingsMenuLanguageCheckboxStates() {
    const { docLangTag, isDocLangTagSupported } =
      await this.#getCachedDetectedLanguages();

    const { panel } = this.elements;
    const alwaysTranslateMenuItems = panel.ownerDocument.querySelectorAll(
      ".always-translate-language-menuitem"
    );
    const neverTranslateMenuItems = panel.ownerDocument.querySelectorAll(
      ".never-translate-language-menuitem"
    );

    if (
      !docLangTag ||
      !isDocLangTagSupported ||
      docLangTag === new Intl.Locale(Services.locale.appLocaleAsBCP47).language
    ) {
      for (const menuitem of alwaysTranslateMenuItems) {
        menuitem.disabled = true;
      }
      for (const menuitem of neverTranslateMenuItems) {
        menuitem.disabled = true;
      }
      return;
    }

    const alwaysTranslateLanguage =
      TranslationsParent.shouldAlwaysTranslateLanguage(docLangTag);
    const neverTranslateLanguage =
      TranslationsParent.shouldNeverTranslateLanguage(docLangTag);

    for (const menuitem of alwaysTranslateMenuItems) {
      menuitem.setAttribute(
        "checked",
        alwaysTranslateLanguage ? "true" : "false"
      );
      menuitem.disabled = false;
    }
    for (const menuitem of neverTranslateMenuItems) {
      menuitem.setAttribute(
        "checked",
        neverTranslateLanguage ? "true" : "false"
      );
      menuitem.disabled = false;
    }
  }

  /**
   * Updates the checked states of the settings menu checkboxes that
   * pertain to site permissions.
   */
  async #updateSettingsMenuSiteCheckboxStates() {
    const { panel } = this.elements;
    const neverTranslateSiteMenuItems = panel.ownerDocument.querySelectorAll(
      ".never-translate-site-menuitem"
    );
    const neverTranslateSite =
      await this.#getTranslationsActor().shouldNeverTranslateSite();

    for (const menuitem of neverTranslateSiteMenuItems) {
      menuitem.setAttribute("checked", neverTranslateSite ? "true" : "false");
    }
  }

  /**
   * Populates the language-related settings menuitems by adding the
   * localized display name of the document's detected language tag.
   */
  async #populateSettingsMenuItems() {
    const { docLangTag } = await this.#getCachedDetectedLanguages();

    const { panel } = this.elements;

    const alwaysTranslateMenuItems = panel.ownerDocument.querySelectorAll(
      ".always-translate-language-menuitem"
    );
    const neverTranslateMenuItems = panel.ownerDocument.querySelectorAll(
      ".never-translate-language-menuitem"
    );

    /** @type {string | undefined} */
    let docLangDisplayName;
    if (docLangTag) {
      const displayNames = new Services.intl.DisplayNames(undefined, {
        type: "language",
        fallback: "none",
      });
      // The display name will still be empty if the docLangTag is not known.
      docLangDisplayName = displayNames.of(docLangTag);
    }

    for (const menuitem of alwaysTranslateMenuItems) {
      if (docLangDisplayName) {
        document.l10n.setAttributes(
          menuitem,
          "translations-panel-settings-always-translate-language",
          { language: docLangDisplayName }
        );
      } else {
        document.l10n.setAttributes(
          menuitem,
          "translations-panel-settings-always-translate-unknown-language"
        );
      }
    }

    for (const menuitem of neverTranslateMenuItems) {
      if (docLangDisplayName) {
        document.l10n.setAttributes(
          menuitem,
          "translations-panel-settings-never-translate-language",
          { language: docLangDisplayName }
        );
      } else {
        document.l10n.setAttributes(
          menuitem,
          "translations-panel-settings-never-translate-unknown-language"
        );
      }
    }

    await Promise.all([
      this.#updateSettingsMenuLanguageCheckboxStates(),
      this.#updateSettingsMenuSiteCheckboxStates(),
    ]);
  }

  /**
   * Configures the panel for the user to reset the page after it has been translated.
   *
   * @param {TranslationPair} translationPair
   */
  async #showRevisitView({ fromLanguage, toLanguage }) {
    const { fromMenuList, toMenuList } = this.elements;

    fromMenuList.value = fromLanguage;
    toMenuList.value = toLanguage;
    this.onChangeLanguages();
  }

  /**
   * Handle the disable logic for when the menulist is changed for the "Translate to"
   * on the "revisit" subview.
   */
  onChangeRevisitTo() {
    const { revisitTranslate, revisitMenuList } = this.elements;
    revisitTranslate.disabled = !revisitMenuList.value;
  }

  /**
   * When changing the language selection, the translate button will need updating.
   */
  onChangeLanguages() {
    this.#updateViewFromTranslationStatus();
  }

  /**
   * When a language is not supported and the menu is manually invoked, an error message
   * is shown. This method switches the panel back to the language selection view.
   * Note that this bypasses the showSubView method since the main view doesn't support
   * a subview.
   */
  async onChangeSourceLanguage(event) {
    const { panel } = this.elements;
    panel.addEventListener("popuphidden", async () => {}, { once: true });
    PanelMultiView.hidePopup(panel);

    await this.#showDefaultView(true /* force this view to be shown */);

    PanelMultiView.openPopup(panel, this.elements.appMenuButton, {
      position: "bottomright topright",
      triggeringEvent: event,
    }).catch(error => this.console.error(error));
  }

  async #reloadLangList() {
    try {
      await this.#ensureLangListsBuilt();
      await this.#showDefaultView();
    } catch (error) {
      this.elements.errorHintAction.disabled = false;
    }
  }

  /**
   * Opens the TranslationsPanel.
   *
   * @param {Event} event
   */
  async open(event) {
    event.stopPropagation();
    if (
      (event.type == "click" && event.button != 0) ||
      (event.type == "keypress" &&
        event.charCode != KeyEvent.DOM_VK_SPACE &&
        event.keyCode != KeyEvent.DOM_VK_RETURN)
    ) {
      // Allow only left click, space, or enter.
      return;
    }

    const { panel, button } = this.elements;

    await this.#ensureLangListsBuilt();

    const { requestedTranslationPair } =
      this.#getTranslationsActor().languageState;

    if (requestedTranslationPair) {
      await this.#showRevisitView(requestedTranslationPair).catch(error => {
        this.console.error(error);
      });
    } else {
      await this.#showDefaultView().catch(error => {
        this.console.error(error);
      });
    }

    this.#populateSettingsMenuItems();

    const [targetButton, openedFromAppMenu] =
      button.contains(event.target) ||
      event.type === "TranslationsParent:OfferTranslation"
        ? [button, false]
        : [this.elements.appMenuButton, true];

    panel.addEventListener(
      "ViewShown",
      () => TranslationsTelemetry.onOpenPanel(openedFromAppMenu),
      { once: true }
    );

    PanelMultiView.openPopup(panel, targetButton, {
      position: "bottomright topright",
      triggerEvent: event,
    }).catch(error => this.console.error(error));
  }

  /**
   * Removes the translations button.
   */
  #hideTranslationsButton() {
    const { button, buttonLocale, buttonCircleArrows } = this.elements;
    button.hidden = true;
    buttonLocale.hidden = true;
    buttonCircleArrows.hidden = true;
    button.removeAttribute("translationsactive");
  }

  /**
   * Returns true if translations is currently active, otherwise false.
   *
   * @returns {boolean}
   */
  #isTranslationsActive() {
    const { requestedTranslationPair } =
      this.#getTranslationsActor().languageState;
    return requestedTranslationPair !== null;
  }

  /**
   * Handle the translation button being clicked when there are two language options.
   */
  async onTranslate() {
    PanelMultiView.hidePopup(this.elements.panel);

    const actor = this.#getTranslationsActor();
    actor.translate(
      this.elements.fromMenuList.value,
      this.elements.toMenuList.value,
      false // reportAsAutoTranslate
    );
  }

  onCancel() {
    PanelMultiView.hidePopup(this.elements.panel);
  }

  /**
   * A handler for opening the settings context menu.
   */
  openSettingsPopup(button) {
    this.#updateSettingsMenuLanguageCheckboxStates();
    this.#updateSettingsMenuSiteCheckboxStates();
    const popup = button.ownerDocument.getElementById(
      "translations-panel-settings-menupopup"
    );
    popup.openPopup(button, "after_end");
  }

  /**
   * Creates a new CheckboxPageAction based on the current translated
   * state of the page and the state of the persistent options in the
   * translations panel settings.
   *
   * @returns {CheckboxPageAction}
   */
  getCheckboxPageActionFor() {
    const {
      alwaysTranslateLanguageMenuItem,
      neverTranslateLanguageMenuItem,
      neverTranslateSiteMenuItem,
    } = this.elements;

    const alwaysTranslateLanguage =
      alwaysTranslateLanguageMenuItem.getAttribute("checked") === "true";
    const neverTranslateLanguage =
      neverTranslateLanguageMenuItem.getAttribute("checked") === "true";
    const neverTranslateSite =
      neverTranslateSiteMenuItem.getAttribute("checked") === "true";

    return new CheckboxPageAction(
      this.#isTranslationsActive(),
      alwaysTranslateLanguage,
      neverTranslateLanguage,
      neverTranslateSite
    );
  }

  /**
   * Redirect the user to about:preferences
   */
  openManageLanguages() {
    const window =
      gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal;
    window.openTrustedLinkIn("about:preferences#general-translations", "tab");
  }

  /**
   * Performs the given page action.
   *
   * @param {PageAction} pageAction
   */
  async #doPageAction(pageAction) {
    switch (pageAction) {
      case PageAction.NO_CHANGE: {
        break;
      }
      case PageAction.RESTORE_PAGE: {
        await this.onRestore();
        break;
      }
      case PageAction.TRANSLATE_PAGE: {
        await this.onTranslate();
        break;
      }
    }
  }

  /**
   * Updates the always-translate-language menuitem prefs and checked state.
   * If auto-translate is currently active for the doc language, deactivates it.
   * If auto-translate is currently inactive for the doc language, activates it.
   */
  async onAlwaysTranslateLanguage() {
    const { docLangTag } = await this.#getCachedDetectedLanguages();
    if (!docLangTag) {
      throw new Error("Expected to have a document language tag.");
    }
    const pageAction =
      this.getCheckboxPageActionFor().alwaysTranslateLanguage();
    TranslationsParent.toggleAlwaysTranslateLanguagePref(docLangTag);
    this.#updateSettingsMenuLanguageCheckboxStates();
    await this.#doPageAction(pageAction);
  }

  /**
   * Updates the never-translate-language menuitem prefs and checked state.
   * If never-translate is currently active for the doc language, deactivates it.
   * If never-translate is currently inactive for the doc language, activates it.
   */
  async onNeverTranslateLanguage() {
    const { docLangTag } = await this.#getCachedDetectedLanguages();
    if (!docLangTag) {
      throw new Error("Expected to have a document language tag.");
    }
    const pageAction = this.getCheckboxPageActionFor().neverTranslateLanguage();
    TranslationsParent.toggleNeverTranslateLanguagePref(docLangTag);
    this.#updateSettingsMenuLanguageCheckboxStates();
    await this.#doPageAction(pageAction);
  }

  /**
   * Updates the never-translate-site menuitem permissions and checked state.
   * If never-translate is currently active for the site, deactivates it.
   * If never-translate is currently inactive for the site, activates it.
   */
  async onNeverTranslateSite() {
    const pageAction = this.getCheckboxPageActionFor().neverTranslateSite();
    await this.#getTranslationsActor().toggleNeverTranslateSitePermissions();
    this.#updateSettingsMenuSiteCheckboxStates();
    await this.#doPageAction(pageAction);
  }

  /**
   * Handle the restore button being clicked.
   */
  async onRestore() {
    const { panel } = this.elements;
    PanelMultiView.hidePopup(panel);
    const { docLangTag } = await this.#getCachedDetectedLanguages();
    if (!docLangTag) {
      throw new Error("Expected to have a document language tag.");
    }

    this.#getTranslationsActor().restorePage(docLangTag);
  }

  handleEventId = 0;

  /**
   * Set the state of the translations button in the URL bar.
   *
   * @param {CustomEvent} event
   */
  handleEvent = async event => {
    switch (event.type) {
      case "TranslationsParent:OfferTranslation": {
        this.open(event);
        break;
      }
      case "TranslationsParent:LanguageState":
        // Check this value after every `await` to guard against race conditions.
        const handleEventId = ++this.handleEventId;
        const {
          detectedLanguages,
          requestedTranslationPair,
          error,
          isEngineReady,
        } = event.detail;

        const { panel, button, buttonLocale, buttonCircleArrows } =
          this.elements;

        const hasSupportedLanguage =
          detectedLanguages?.docLangTag &&
          detectedLanguages?.userLangTag &&
          detectedLanguages?.isDocLangTagSupported;

        if (detectedLanguages) {
          // Ensure the cached detected languages are up to date, for instance whenever
          // the user switches tabs.
          TranslationsPanel.detectedLanguages = detectedLanguages;
        }

        this.#updateViewFromTranslationStatus();

        if (
          // We've already requested to translate this page, so always show the icon.
          requestedTranslationPair ||
          // There was an error translating, so always show the icon. This can happen
          // when a user manually invokes the translation and we wouldn't normally show
          // the icon.
          error ||
          // Finally check that we can translate this language.
          (hasSupportedLanguage &&
            (await TranslationsParent.getIsTranslationsEngineSupported()))
        ) {
          if (handleEventId !== this.handleEventId) {
            // A new handleEvent was received, this one is stale.
            return;
          }
          button.hidden = false;
          if (requestedTranslationPair) {
            // The translation is active, update the urlbar button.
            button.setAttribute("translationsactive", true);
            if (isEngineReady) {
              const displayNames = new Services.intl.DisplayNames(undefined, {
                type: "language",
              });

              document.l10n.setAttributes(
                button,
                "urlbar-translations-button-translated",
                {
                  fromLanguage: displayNames.of(
                    requestedTranslationPair.fromLanguage
                  ),
                  toLanguage: displayNames.of(
                    requestedTranslationPair.toLanguage
                  ),
                }
              );
              // Show the locale of the page in the button.
              buttonLocale.hidden = false;
              buttonCircleArrows.hidden = true;
              buttonLocale.innerText = requestedTranslationPair.toLanguage;
            } else {
              document.l10n.setAttributes(
                button,
                "urlbar-translations-button-loading"
              );
              // Show the spinning circle arrows to indicate that the engine is
              // still loading.
              buttonCircleArrows.hidden = false;
              buttonLocale.hidden = true;
            }
          } else {
            // The translation is not active, update the urlbar button.
            button.removeAttribute("translationsactive");
            document.l10n.setAttributes(button, "urlbar-translations-button");
            buttonLocale.hidden = true;
            buttonCircleArrows.hidden = true;
          }
        } else {
          if (handleEventId !== this.handleEventId) {
            // A new handleEvent was received, this one is stale.
            return;
          }
          this.#hideTranslationsButton();
        }

        switch (error) {
          case null:
            this.elements.error.hidden = true;
            break;
          case "engine-load-failure":
            this.elements.error.hidden = false;
            this.#showError({
              message: "translations-panel-error-translating",
            });
            const targetButton = button.hidden
              ? this.elements.appMenuButton
              : button;

            // Re-open the menu on an error.
            PanelMultiView.openPopup(panel, targetButton, {
              position: "bottomright topright",
            }).catch(panelError => this.console.error(panelError));

            break;
          default:
            console.error("Unknown translation error", error);
        }
        break;
    }
  };
})();
