/* 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-globals-from preferences.js */

/**
 * @typedef {import("../../../toolkit/components/translations/translations").SupportedLanguages} SupportedLanguages
 */

/**
 * The permission type to give to Services.perms for Translations.
 */
const TRANSLATIONS_PERMISSION = "translations";

/**
 * The list of BCP-47 language tags that will trigger auto-translate.
 */
const ALWAYS_TRANSLATE_LANGS_PREF =
  "browser.translations.alwaysTranslateLanguages";

/**
 * The list of BCP-47 language tags that will prevent auto-translate.
 */
const NEVER_TRANSLATE_LANGS_PREF =
  "browser.translations.neverTranslateLanguages";

/**
 * The topic fired to observers when a pref related to Translations changes.
 */
const TOPIC_TRANSLATIONS_PREF_CHANGED = "translations:pref-changed";

let gTranslationsPane = {
  /**
   * List of languages set in the Always Translate Preferences
   *
   * @type Array<string>
   */
  alwaysTranslateLanguages: [],

  /**
   * List of languages set in the Never Translate Preferences
   *
   * @type Array<string>
   */
  neverTranslateLanguages: [],

  /**
   * List of languages set in the Never Translate Site Preferences
   *
   * @type Array<string>
   */
  neverTranslateSites: [],

  /**
   * A mapping from the language tag to the current download phase for that language
   * and it's download size.
   *
   * @type {Map<string, {downloadPhase: "downloaded" | "removed" | "loading", size: number}>}
   */
  downloadPhases: new Map(),

  /**
   * Object with details of languages supported by the browser.
   *
   * @type {SupportedLanguages}
   */
  supportedLanguages: {},

  /**
   * List of languages names supported along with their tags (BCP 47 locale identifiers).
   *
   * @type Array<{ langTag: string, displayName: string}>
   */
  supportedLanguageTagsNames: [],

  /**
   * Add Lazy getter for document elements
   */
  elements: undefined,

  async init() {
    if (!this.elements) {
      this._defineLazyElements(document, {
        downloadLanguageSection: "translations-settings-download-section",
        alwaysTranslateMenuList: "translations-settings-always-translate-list",
        neverTranslateMenuList: "translations-settings-never-translate-list",
        alwaysTranslateMenuPopup:
          "translations-settings-always-translate-popup",
        neverTranslateMenuPopup: "translations-settings-never-translate-popup",
        downloadLanguageList: "translations-settings-download-language-list",
        alwaysTranslateLanguageList:
          "translations-settings-always-translate-language-list",
        neverTranslateLanguageList:
          "translations-settings-never-translate-language-list",
        neverTranslateSiteList:
          "translations-settings-never-translate-site-list",
        translationsSettingsBackButton: "translations-settings-back-button",
        translationsSettingsHeader: "translations-settings-header",
        translationsSettingsDescription: "translations-settings-description",
        translateAlwaysHeader: "translations-settings-always-translate",
        translateNeverHeader: "translations-settings-never-translate",
        translateNeverSiteHeader: "translations-settings-never-sites-header",
        translateNeverSiteDesc: "translations-settings-never-sites",
        translateDownloadLanguagesLearnMore: "download-languages-learn-more",
      });
    }
    this.elements.translationsSettingsBackButton.addEventListener(
      "click",
      function () {
        gotoPref("general");
      }
    );

    // Keyboard navigation support.
    this.elements.alwaysTranslateMenuList.addEventListener("keydown", this);
    this.elements.alwaysTranslateMenuPopup.addEventListener(
      "popuphidden",
      this
    );
    this.elements.neverTranslateMenuList.addEventListener("keydown", this);
    this.elements.neverTranslateMenuPopup.addEventListener("popuphidden", this);

    // Get the settings from the preferences into the translations.js
    this.supportedLanguages = await TranslationsParent.getSupportedLanguages();
    this.supportedLanguageTagsNames = TranslationsParent.getLanguageList(
      this.supportedLanguages
    );

    this.neverTranslateSites = TranslationsParent.listNeverTranslateSites();

    // Deploy observers
    Services.obs.addObserver(this, "perm-changed");
    Services.obs.addObserver(this, TOPIC_TRANSLATIONS_PREF_CHANGED);
    window.addEventListener("unload", () => this.removeObservers());

    // Build the HTML elements
    this.buildLanguageDropDowns();
    // Keyboard navigation support.
    this.elements.alwaysTranslateLanguageList.addEventListener("keydown", this);
    this.elements.neverTranslateLanguageList.addEventListener("keydown", this);
    this.elements.neverTranslateSiteList.addEventListener("keydown", this);
    this.populateLanguageList(ALWAYS_TRANSLATE_LANGS_PREF);
    this.populateLanguageList(NEVER_TRANSLATE_LANGS_PREF);
    this.populateSiteList();

    await this.initDownloadInfo();
    this.buildDownloadLanguageList();

    // The translations settings page takes a long time to initialize
    // This event can be used to wait until the initialization is done.
    document.dispatchEvent(
      new CustomEvent("translationsSettingsInit", {
        bubbles: true,
        cancelable: true,
      })
    );
  },

  _defineLazyElements(document, entries) {
    this.elements = {};
    for (const [name, elementId] of Object.entries(entries)) {
      ChromeUtils.defineLazyGetter(this.elements, name, () => {
        const element = document.getElementById(elementId);
        if (!element) {
          throw new Error(`Could not find "${name}" at "#${elementId}".`);
        }
        return element;
      });
    }
  },

  /**
   * Populate the Drop down list in <menupopup> with the list of supported languages
   * for the user to choose languages to add to Always translate and
   * Never translate settings list.
   */
  buildLanguageDropDowns() {
    const { sourceLanguages } = this.supportedLanguages;
    const { alwaysTranslateMenuPopup, neverTranslateMenuPopup } = this.elements;

    for (const { langTag, displayName } of sourceLanguages) {
      const alwaysLang = document.createXULElement("menuitem");
      alwaysLang.setAttribute("value", langTag);
      alwaysLang.setAttribute("label", displayName);
      alwaysTranslateMenuPopup.appendChild(alwaysLang);
      const neverLang = document.createXULElement("menuitem");
      neverLang.setAttribute("value", langTag);
      neverLang.setAttribute("label", displayName);
      neverTranslateMenuPopup.appendChild(neverLang);
    }
  },

  /**
   * Initializes the downloadPhases by checking the download status of each language.
   *
   * @see gTranslationsPane.downloadPhases
   */
  async initDownloadInfo() {
    let downloadCount = 0;
    let allDownloadSize = 0;

    this.downloadPhases = new Map();
    for (const language of this.supportedLanguageTagsNames) {
      let downloadSize = await TranslationsParent.getLanguageSize(
        language.langTag
      );
      allDownloadSize += downloadSize;
      const hasAllFilesForLanguage =
        await TranslationsParent.hasAllFilesForLanguage(language.langTag);
      const downloadPhase = hasAllFilesForLanguage ? "downloaded" : "removed";
      this.downloadPhases.set(language.langTag, {
        downloadPhase,
        size: downloadSize,
      });
      downloadCount += downloadPhase === "downloaded" ? 1 : 0;
    }
    const allDownloadPhase =
      downloadCount === this.supportedLanguageTagsNames.length
        ? "downloaded"
        : "removed";
    this.downloadPhases.set("all", {
      downloadPhase: allDownloadPhase,
      size: allDownloadSize,
    });
  },

  /**
   * Show a list of languages for the user to be able to download
   * and remove language models for local translation.
   */
  buildDownloadLanguageList() {
    const { downloadLanguageList } = this.elements;

    function createSizeElement(downloadSize) {
      const languageSize = document.createElement("span");
      languageSize.classList.add("translations-settings-download-size");
      const [size, units] = DownloadUtils.convertByteUnits(downloadSize);

      document.l10n.setAttributes(
        languageSize,
        "translations-settings-download-size",
        {
          size: size + " " + units,
        }
      );
      return languageSize;
    }

    // The option to download "All languages" is added in xhtml.
    // Here the option to download individual languages is dynamically added
    // based on the supported language list
    const allLangElement = downloadLanguageList.firstElementChild;
    let allLangButton = allLangElement.querySelector("moz-button");

    // The first element is selected by default when keyboard navigation enters this list
    downloadLanguageList.setAttribute(
      "aria-activedescendant",
      allLangElement.id
    );
    // Keyboard navigation support.
    downloadLanguageList.addEventListener("keydown", this);
    allLangButton.addEventListener("click", this);
    allLangElement.addEventListener("keydown", this);

    for (const language of this.supportedLanguageTagsNames) {
      const downloadSize = this.downloadPhases.get(language.langTag).size;

      const languageSize = createSizeElement(downloadSize);

      const languageLabel = this.createLangLabel(
        language.displayName,
        language.langTag,
        "translations-settings-download-" + language.langTag
      );

      const isDownloaded =
        this.downloadPhases.get(language.langTag).downloadPhase ===
        "downloaded";

      const mozButton = isDownloaded
        ? this.createIconButton(
            [
              "translations-settings-remove-icon",
              "translations-settings-manage-downloaded-language-button",
            ],
            "translations-settings-remove-button",
            language.displayName
          )
        : this.createIconButton(
            [
              "translations-settings-download-icon",
              "translations-settings-manage-downloaded-language-button",
            ],
            "translations-settings-download-button",
            language.displayName
          );

      const languageElement = this.createLangElement(
        [mozButton, languageLabel, languageSize],
        "translations-settings-download-" + language.langTag + "-language-id"
      );
      downloadLanguageList.appendChild(languageElement);
    }

    // Updating "All Language" download button according to the state
    if (this.downloadPhases.get("all").downloadPhase === "downloaded") {
      this.changeButtonState({
        langButton: allLangButton,
        langTag: "all",
        langState: "downloaded",
      });
    }

    const allDownloadSize = this.downloadPhases.get("all").size;
    const languageSize = createSizeElement(allDownloadSize);

    allLangElement.appendChild(languageSize);
  },

  handleEvent(event) {
    const eventNode = event.target;
    const eventNodeParent = eventNode.parentNode;
    const eventNodeClassList = eventNode.classList;
    for (const err of document.querySelectorAll(
      ".translations-settings-language-error"
    )) {
      this.removeError(err);
    }

    switch (event.type) {
      case "keydown":
        // Keyboard navigation support.
        this.handleKeys(event);
        break;
      case "popuphidden":
        // Handle Menulist selection through pointing device
        if (
          eventNodeParent.id === "translations-settings-always-translate-list"
        ) {
          this.handleAddAlwaysTranslateLanguage(
            event.target.parentNode.getAttribute("value")
          );
        } else if (
          eventNodeParent.id === "translations-settings-never-translate-list"
        ) {
          this.handleAddNeverTranslateLanguage(
            event.target.parentNode.getAttribute("value")
          );
        }
        break;
      case "click":
        if (eventNodeClassList.contains("translations-settings-site-button")) {
          this.handleRemoveNeverTranslateSite(event);
        } else if (
          eventNodeClassList.contains(
            "translations-settings-language-never-button"
          )
        ) {
          this.handleRemoveNeverTranslateLanguage(event);
        } else if (
          eventNodeClassList.contains(
            "translations-settings-language-always-button"
          )
        ) {
          this.handleRemoveAlwaysTranslateLanguage(event);
        } else if (
          eventNodeClassList.contains(
            "translations-settings-manage-downloaded-language-button"
          )
        ) {
          if (
            eventNodeClassList.contains("translations-settings-download-icon")
          ) {
            if (
              eventNodeParent.querySelector("label").id ===
              "translations-settings-download-all-languages"
            ) {
              this.handleDownloadAllLanguages(event);
            } else {
              this.handleDownloadLanguage(event);
            }
          } else if (
            eventNodeClassList.contains("translations-settings-remove-icon")
          ) {
            if (
              eventNodeParent.querySelector("label").id ===
              "translations-settings-download-all-languages"
            ) {
              this.handleRemoveAllDownloadLanguages(event);
            } else {
              this.handleRemoveDownloadLanguage(event);
            }
          }
        }
        break;
    }
  },

  // Keyboard navigation support.
  handleKeys(event) {
    switch (event.key) {
      case "Enter":
        // Handle Menulist selection through keyboard
        if (event.target.id === "translations-settings-always-translate-list") {
          this.handleAddAlwaysTranslateLanguage(
            event.target.getAttribute("value")
          );
        } else if (
          event.target.id === "translations-settings-never-translate-list"
        ) {
          this.handleAddNeverTranslateLanguage(
            event.target.getAttribute("value")
          );
        }
        break;
      case "ArrowUp":
        if (
          event.target.classList.contains("translations-settings-language-list")
        ) {
          event.target.children[0].querySelector("moz-button").focus();
          // Update the selected element on the list according to the keyboard navigation by the user
          event.target.setAttribute(
            "aria-activedescendant",
            event.target.children[0].id
          );
        } else if (event.target.tagName === "moz-button") {
          if (event.target.parentNode.previousElementSibling) {
            event.target.parentNode.previousElementSibling
              .querySelector("moz-button")
              .focus();
            // Update the selected element on the list according to the keyboard navigation by the user
            event.target.parentNode.parentNode.setAttribute(
              "aria-activedescendant",
              event.target.parentNode.previousElementSibling.id
            );
            event.preventDefault();
          }
        }
        break;
      case "ArrowDown":
        if (
          event.target.classList.contains("translations-settings-language-list")
        ) {
          event.target.children[0].querySelector("moz-button").focus();
          // Update the selected element on the list according to the keyboard navigation by the user
          event.target.setAttribute(
            "aria-activedescendant",
            event.target.children[0].id
          );
        } else if (event.target.tagName === "moz-button") {
          if (event.target.parentNode.nextElementSibling) {
            event.target.parentNode.nextElementSibling
              .querySelector("moz-button")
              .focus();
            // Update the selected element on the list according to the keyboard navigation by the user
            event.target.parentNode.parentNode.setAttribute(
              "aria-activedescendant",
              event.target.parentNode.nextElementSibling.id
            );
            event.preventDefault();
          }
        }
        break;
    }
  },

  /**
   * Event handler when the user wants to add a language to
   * Always translate settings preferences list.
   *
   * @param {Event} event
   */
  async handleAddAlwaysTranslateLanguage(langTag) {
    // After a language is selected the menulist button display will be set to the
    // selected langauge. After processing the button event the
    // data-l10n-id of the menulist button is restored to "Add Language"

    const { alwaysTranslateMenuList } = this.elements;
    TranslationsParent.addLangTagToPref(langTag, ALWAYS_TRANSLATE_LANGS_PREF);
    await document.l10n.translateElements([alwaysTranslateMenuList]);
  },

  /**
   * Event handler when the user wants to add a language to
   * Never translate settings preferences list.
   *
   * @param {Event} event
   */
  async handleAddNeverTranslateLanguage(langTag) {
    // After a language is selected the menulist button display will be set to the
    // selected langauge. After processing the button event the
    // data-l10n-id of the menulist button is restored to "Add Language"

    const { neverTranslateMenuList } = this.elements;

    TranslationsParent.addLangTagToPref(langTag, NEVER_TRANSLATE_LANGS_PREF);
    await document.l10n.translateElements([neverTranslateMenuList]);
  },

  /**
   * Finds the langauges added and/or removed in the
   * Always/Never translate lists.
   *
   * @param {Array<string>} currentSet
   * @param {Array<string>} newSet
   * @returns {object} {Array<string>, Array<string>}
   */
  setDifference(currentSet, newSet) {
    const added = newSet.filter(lang => !currentSet.includes(lang));
    const removed = currentSet.filter(lang => !newSet.includes(lang));
    return { added, removed };
  },

  /**
   * Builds HTML elements for the Always/Never translate list
   * According to the preference setting
   *
   * @param {string} pref - name of the preference for which the HTML is built
   *                      NEVER_TRANSLATE_LANGS_PREF / ALWAYS_TRANSLATE_LANGS_PREF
   */
  populateLanguageList(pref) {
    // languageList: <div> of the Always/Never translate section, which is a list of languages added by the user
    // curLangTags: List of Language tag set in the the preference, Always/Never translate to be populated
    // otherPref: name of the preference other than "pref" Never/Always
    //            when a language is added to "pref" remove the same from otherPref(if it exists)
    // prefix: "always"/"never" string used to create ids for the language HTML elements for respective lists.

    const { languageList, curLangTags, otherPref, prefix } =
      pref === NEVER_TRANSLATE_LANGS_PREF
        ? {
            languageList: this.elements.neverTranslateLanguageList,
            curLangTags: Array.from(this.neverTranslateLanguages),
            otherPref: ALWAYS_TRANSLATE_LANGS_PREF,
            prefix: "never",
          }
        : {
            languageList: this.elements.alwaysTranslateLanguageList,
            curLangTags: Array.from(this.alwaysTranslateLanguages),
            otherPref: NEVER_TRANSLATE_LANGS_PREF,
            prefix: "always",
          };

    const updatedLangTags =
      pref === NEVER_TRANSLATE_LANGS_PREF
        ? Array.from(TranslationsParent.getNeverTranslateLanguages())
        : Array.from(TranslationsParent.getAlwaysTranslateLanguages());

    const { added, removed } = this.setDifference(curLangTags, updatedLangTags);

    for (const lang of removed) {
      this.removeTranslateLanguage(lang, languageList);
    }

    // When the preferences is opened for the first time
    // the translations settings HTML page is initialized with
    // the existing settings by adding all languages from the latest preferences
    for (const lang of added) {
      this.addTranslateLanguage(lang, languageList, prefix);
      // if a language is added to Always translate list,
      // remove it from Never translate list and vice-versa
      TranslationsParent.removeLangTagFromPref(lang, otherPref);
    }

    // Update state for neverTranslateLanguages/alwaysTranslateLanguages
    if (pref === NEVER_TRANSLATE_LANGS_PREF) {
      this.neverTranslateLanguages = updatedLangTags;
    } else {
      this.alwaysTranslateLanguages = updatedLangTags;
    }
  },

  /**
   * Adds a site to Never translate site list
   *
   * @param {string} site
   */
  addSite(site) {
    const { neverTranslateSiteList } = this.elements;

    // Label and textContent of the added site element is the same
    const languageLabel = this.createLangLabel(
      site,
      site,
      "translations-settings-" + site
    );

    const mozButton = this.createIconButton(
      [
        "translations-settings-remove-icon",
        "translations-settings-site-button",
      ],
      "translations-settings-remove-site-button-2",
      site
    );

    // Create unique id using site name
    const languageElement = this.createLangElement(
      [mozButton, languageLabel],
      "translations-settings-" + site + "-id"
    );
    neverTranslateSiteList.insertBefore(
      languageElement,
      neverTranslateSiteList.firstElementChild
    );
    // The first element is selected by default when keyboard navigation enters this list
    neverTranslateSiteList.setAttribute(
      "aria-activedescendant",
      languageElement.id
    );
    if (neverTranslateSiteList.childElementCount) {
      neverTranslateSiteList.parentNode.hidden = false;
    }
  },

  /**
   * Removes a site from Never translate site list
   *
   * @param {string} site
   */
  removeSite(site) {
    const { neverTranslateSiteList } = this.elements;

    const langSite = neverTranslateSiteList.querySelector(
      `label[value="${site}"]`
    );

    langSite.parentNode.remove();
    if (!neverTranslateSiteList.childElementCount) {
      neverTranslateSiteList.parentNode.hidden = true;
    }
  },

  /**
   * Builds HTML elements for the Never translate Site list
   * According to the permissions setting
   */
  populateSiteList() {
    const siteList = TranslationsParent.listNeverTranslateSites();
    for (const site of siteList) {
      this.addSite(site);
    }
    this.neverTranslateSites = siteList;
  },

  /**
   * Oberver
   *
   * @param {string} subject Notification specific interface pointer.
   * @param {string} topic nsPref:changed/perm-changed
   * @param {string} data cleared/changed/added/deleted
   */
  observe(subject, topic, data) {
    if (topic === "perm-changed") {
      if (data === "cleared") {
        const { neverTranslateSiteList } = this.elements;
        this.neverTranslateSites = [];
        for (const elem of neverTranslateSiteList.children) {
          elem.remove();
        }
        if (!neverTranslateSiteList.childElementCount) {
          neverTranslateSiteList.parentNode.hidden = true;
        }
      } else {
        const perm = subject.QueryInterface(Ci.nsIPermission);
        if (perm.type != TRANSLATIONS_PERMISSION) {
          // The updated permission was not for Translations, nothing to do.
          return;
        }
        if (data === "added") {
          if (perm.capability != Services.perms.DENY_ACTION) {
            // We are only showing data for sites we should never translate.
            // If the permission is not DENY_ACTION, we don't care about it here.
            return;
          }
          this.neverTranslateSites =
            TranslationsParent.listNeverTranslateSites();
          this.addSite(perm.principal.origin);
        } else if (data === "deleted") {
          this.neverTranslateSites =
            TranslationsParent.listNeverTranslateSites();
          this.removeSite(perm.principal.origin);
        }
      }
    } else if (topic === TOPIC_TRANSLATIONS_PREF_CHANGED) {
      switch (data) {
        case ALWAYS_TRANSLATE_LANGS_PREF:
        case NEVER_TRANSLATE_LANGS_PREF: {
          this.populateLanguageList(data);
          break;
        }
      }
    }
  },

  /**
   * Removes Observers
   */
  removeObservers() {
    Services.obs.removeObserver(this, "perm-changed");
    Services.obs.removeObserver(this, TOPIC_TRANSLATIONS_PREF_CHANGED);
  },

  /**
   * Create a div HTML element representing a language.
   *
   * @param {Array} langChildren
   * @returns {Element} div HTML element
   */
  createLangElement(langChildren, langId) {
    const languageElement = document.createElement("div");
    languageElement.classList.add("translations-settings-language");
    // Keyboard navigation support
    languageElement.setAttribute("role", "option");
    languageElement.id = langId;
    languageElement.addEventListener("keydown", this);

    for (const child of langChildren) {
      languageElement.appendChild(child);
    }
    return languageElement;
  },

  /**
   * Creates a moz-button element as icon
   *
   * @param {string} classNames classes added to the moz-button element
   * @param {string} buttonFluentID Fluent ID for the aria-label
   * @param {string} accessibleName  "name" variable value of the aria-label
   * @returns {Element} HTML element of type Moz-Button
   */
  createIconButton(classNames, buttonFluentID, accessibleName) {
    const mozButton = document.createElement("moz-button");

    for (const className of classNames) {
      mozButton.classList.add(className);
    }
    mozButton.setAttribute("type", "ghost icon");
    // Note: aria-labelledby cannot be used as the id is not available for the shadow DOM element
    document.l10n.setAttributes(mozButton, buttonFluentID, {
      name: accessibleName,
    });
    mozButton.addEventListener("click", this);
    // Keyboard navigation support. Do not select the buttons on the list using tab.
    // The buttons in the language lists are navigated using arrow buttons
    mozButton.setAttribute("tabindex", "-1");
    return mozButton;
  },

  /**
   * Adds a language selected by the user to the list of
   * Always/Never translate settings list in the HTML.
   *
   * @param {string} langTag - The BCP-47 language tag for the language
   * @param {Element} languageList - HTML element for the list of the languages.
   * @param {string} translatePrefix - "never" / "always" prefix depending on the settings section
   */
  addTranslateLanguage(langTag, languageList, translatePrefix) {
    // While adding the first language, add the Header and language List div
    const languageDisplayNames =
      TranslationsParent.createLanguageDisplayNames();

    let languageDisplayName;
    try {
      languageDisplayName = languageDisplayNames.of(langTag);
    } catch (error) {
      console.warn(
        `Failed to retrieve language display name for '${langTag}'.`
      );
      return;
    }

    const languageLabel = this.createLangLabel(
      languageDisplayName,
      langTag,
      "translations-settings-language-" + translatePrefix + "-" + langTag
    );

    const mozButton = this.createIconButton(
      [
        "translations-settings-remove-icon",
        "translations-settings-language-" + translatePrefix + "-button",
      ],
      "translations-settings-remove-language-button-2",
      languageDisplayName
    );

    const languageElement = this.createLangElement(
      [mozButton, languageLabel],
      "translations-settings-language-" +
        translatePrefix +
        "-" +
        langTag +
        "-id"
    );
    // Add the language after the Language Header
    languageList.insertBefore(languageElement, languageList.firstElementChild);
    // The first element is selected by default when keyboard navigation enters this list
    languageList.setAttribute("aria-activedescendant", languageElement.id);
    if (languageList.childElementCount) {
      languageList.parentNode.hidden = false;
    }
  },

  /**
   * Creates a label HTML element representing
   * a language
   *
   * @param {string} textContent
   * @param {string} value
   * @param {string} id
   * @returns {Element} HTML element of type label
   */
  createLangLabel(textContent, value, id) {
    const languageLabel = document.createElement("label");
    languageLabel.textContent = textContent;
    languageLabel.setAttribute("value", value);
    languageLabel.id = id;
    return languageLabel;
  },

  /**
   * Removes a language currently in the always/never translate language list
   * from the DOM. Invoked in response to changes in the relevant preferences.
   *
   * @param {string} langTag The BCP-47 language tag for the language
   * @param {Element} languageList - HTML element for the list of the languages.
   */
  removeTranslateLanguage(langTag, languageList) {
    const langElem = languageList.querySelector(`label[value=${langTag}]`);
    if (langElem) {
      langElem.parentNode.remove();
    }
    if (!languageList.childElementCount) {
      languageList.parentNode.hidden = true;
    }
  },

  /**
   * Event Handler to remove a language selected by the user from the list of
   * Always translate settings list in Preferences.
   *
   * @param {Event} event
   */
  handleRemoveAlwaysTranslateLanguage(event) {
    TranslationsParent.removeLangTagFromPref(
      event.target.parentNode.querySelector("label").getAttribute("value"),
      ALWAYS_TRANSLATE_LANGS_PREF
    );
  },

  /**
   * Event Handler to remove a language selected by the user from the list of
   * Never translate settings list in Preferences.
   *
   * @param {Event} event
   */
  handleRemoveNeverTranslateLanguage(event) {
    TranslationsParent.removeLangTagFromPref(
      event.target.parentNode.querySelector("label").getAttribute("value"),
      NEVER_TRANSLATE_LANGS_PREF
    );
  },

  /**
   * Removes the site chosen by the user in the HTML
   * from the Never Translate Site Permission
   *
   * @param {Event} event
   */
  handleRemoveNeverTranslateSite(event) {
    TranslationsParent.setNeverTranslateSiteByOrigin(
      false,
      event.target.parentNode.querySelector("label").getAttribute("value")
    );
  },
  /**
   * Record the download phase downloaded/loading/removed for
   * given language in the local data.
   *
   * @param {string} langTag
   * @param {string} downloadPhase
   */
  updateDownloadPhase(langTag, downloadPhase) {
    if (!this.downloadPhases.has(langTag)) {
      console.error(
        `Expected downloadPhases entry for ${langTag}, but found none.`
      );
    } else {
      this.downloadPhases.get(langTag).downloadPhase = downloadPhase;
    }
  },
  /**
   * Updates the button icons and its download states for the download language elements
   * in the HTML by getting the download status of all languages from the browser records.
   */
  async reloadDownloadPhases() {
    let downloadCount = 0;
    const { downloadLanguageList } = this.elements;

    const allLangElem = downloadLanguageList.firstElementChild;
    const allLangButton = allLangElem.querySelector("moz-button");

    const updatePromises = [];
    for (const langElem of downloadLanguageList.querySelectorAll(
      ".translations-settings-language:not(:first-child)"
    )) {
      const langLabel = langElem.querySelector("label");
      const langTag = langLabel.getAttribute("value");
      const langButton = langElem.querySelector("moz-button");

      updatePromises.push(
        TranslationsParent.hasAllFilesForLanguage(langTag).then(
          hasAllFilesForLanguage => {
            if (hasAllFilesForLanguage) {
              downloadCount += 1;
              this.changeButtonState({
                langButton,
                langTag,
                langState: "downloaded",
              });
            } else {
              this.changeButtonState({
                langButton,
                langTag,
                langState: "removed",
              });
            }
            langButton.removeAttribute("disabled");
          }
        )
      );
    }
    await Promise.allSettled(updatePromises);

    const allDownloaded =
      downloadCount === this.supportedLanguageTagsNames.length;
    if (allDownloaded) {
      this.changeButtonState({
        langButton: allLangButton,
        langTag: "all",
        langState: "downloaded",
      });
    } else {
      this.changeButtonState({
        langButton: allLangButton,
        langTag: "all",
        langState: "removed",
      });
    }
  },

  showErrorMessage(parentNode, fluentId, language) {
    const errorElement = document.createElement("moz-message-bar");
    errorElement.setAttribute("type", "error");
    document.l10n.setAttributes(errorElement, fluentId, {
      name: language,
    });
    errorElement.classList.add("translations-settings-language-error");
    parentNode.appendChild(errorElement);
  },

  removeError(errorNode) {
    errorNode?.remove();
  },

  /**
   * Event Handler to download a language model selected by the user through HTML
   *
   * @param {Event} event
   */
  async handleDownloadLanguage(event) {
    let eventButton = event.target;
    const langTag = eventButton.parentNode
      .querySelector("label")
      .getAttribute("value");

    this.changeButtonState({
      langButton: eventButton,
      langTag,
      langState: "loading",
    });

    try {
      await TranslationsParent.downloadLanguageFiles(langTag);
    } catch (error) {
      console.error(error);

      const languageDisplayNames =
        TranslationsParent.createLanguageDisplayNames();

      this.showErrorMessage(
        eventButton.parentNode,
        "translations-settings-language-download-error",
        languageDisplayNames.of(langTag)
      );
      const hasAllFilesForLanguage =
        await TranslationsParent.hasAllFilesForLanguage(langTag);

      if (!hasAllFilesForLanguage) {
        this.changeButtonState({
          langButton: eventButton,
          langTag,
          langState: "removed",
        });
        return;
      }
    }
    this.changeButtonState({
      langButton: eventButton,
      langTag,
      langState: "downloaded",
    });

    // If all languages are downloaded, change "All Languages" to downloaded
    const haveRemovedItem = [...this.downloadPhases].some(
      ([k, v]) => v.downloadPhase != "downloaded" && k != "all"
    );
    if (
      !haveRemovedItem &&
      this.downloadPhases.get("all").downloadPhase !== "downloaded"
    ) {
      this.changeButtonState({
        langButton:
          this.elements.downloadLanguageList.firstElementChild.querySelector(
            "moz-button"
          ),
        langTag: "all",
        langState: "downloaded",
      });
    }
  },

  /**
   * Event Handler to remove a language model selected by the user through HTML
   *
   * @param {Event} event
   */
  async handleRemoveDownloadLanguage(event) {
    let eventButton = event.target;
    const langTag = eventButton.parentNode
      .querySelector("label")
      .getAttribute("value");

    this.changeButtonState({
      langButton: eventButton,
      langTag,
      langState: "loading",
    });

    try {
      await TranslationsParent.deleteLanguageFiles(langTag);
    } catch (error) {
      // The download phases are invalidated with the error and must be reloaded.
      console.error(error);

      const languageDisplayNames =
        TranslationsParent.createLanguageDisplayNames();

      this.showErrorMessage(
        eventButton.parentNode,
        "translations-settings-language-remove-error",
        languageDisplayNames.of(langTag)
      );
      const hasAllFilesForLanguage =
        await TranslationsParent.hasAllFilesForLanguage(langTag);
      if (hasAllFilesForLanguage) {
        this.changeButtonState({
          langButton: eventButton,
          langTag,
          langState: "downloaded",
        });
        return;
      }
    }

    this.changeButtonState({
      langButton: eventButton,
      langTag,
      langState: "removed",
    });

    // If >=1 languages are removed change "All Languages" state to removed
    if (this.downloadPhases.get("all").downloadPhase === "downloaded") {
      this.changeButtonState({
        langButton:
          this.elements.downloadLanguageList.firstElementChild.querySelector(
            "moz-button"
          ),
        langTag: "all",
        langState: "removed",
      });
    }
  },

  /**
   * Event Handler to download all language models
   *
   * @param {Event} event
   */
  async handleDownloadAllLanguages(event) {
    // Disable all buttons and show loading icon
    this.disableDownloadButtons();
    let eventButton = event.target;
    this.changeButtonState({
      langButton: eventButton,
      langTag: "all",
      langState: "loading",
    });

    try {
      await TranslationsParent.downloadAllFiles();
    } catch (error) {
      console.error(error);
      await this.reloadDownloadPhases();
      this.showErrorMessage(
        eventButton.parentNode,
        "translations-settings-language-download-error",
        "all"
      );
      return;
    }
    this.changeButtonState({
      langButton: eventButton,
      langTag: "all",
      langState: "downloaded",
    });
    this.updateAllLanguageDownloadButtons("downloaded");
  },

  /**
   * Event Handler to remove all language models
   *
   * @param {Event} event
   */
  async handleRemoveAllDownloadLanguages(event) {
    let eventButton = event.target;
    this.disableDownloadButtons();
    this.changeButtonState({
      langButton: eventButton,
      langTag: "all",
      langState: "loading",
    });

    try {
      await TranslationsParent.deleteAllLanguageFiles();
    } catch (error) {
      console.error(error);
      await this.reloadDownloadPhases();
      this.showErrorMessage(
        eventButton.parentNode,
        "translations-settings-language-remove-error",
        "all"
      );
      return;
    }
    this.changeButtonState({
      langButton: eventButton,
      langTag: "all",
      langState: "removed",
    });
    this.updateAllLanguageDownloadButtons("removed");
  },

  /**
   * Disables the buttons to download/remove inidividual languages
   * when "all languages" are downloaded/removed.
   * This is done to ensure that no individual languages are downloaded/removed
   * when the download/remove operations for "all languages" is progress.
   */
  disableDownloadButtons() {
    const { downloadLanguageList } = this.elements;

    // Disable all elements except the first one which is "All langauges"
    for (const langElem of downloadLanguageList.querySelectorAll(
      ".translations-settings-language:not(:first-child)"
    )) {
      const langButton = langElem.querySelector("moz-button");
      langButton.setAttribute("disabled", "true");
    }
  },

  /**
   * Changes the state of all individual language buttons as downloaded/removed
   * based on the download state of "All Language" status
   * changes the icon of individual language buttons:
   * from "download" icon to "remove" icon if "All Language" is downloaded.
   * from "remove" icon to "download" icon if "All Language" is removed.
   *
   * @param {string} allLanguageDownloadStatus "All Language" status: downloaded/removed
   */
  updateAllLanguageDownloadButtons(allLanguageDownloadStatus) {
    const { downloadLanguageList } = this.elements;

    // Change the state of all individual language buttons except the first one which is "All langauges"
    for (const langElem of downloadLanguageList.querySelectorAll(
      ".translations-settings-language:not(:first-child)"
    )) {
      let langButton = langElem.querySelector("moz-button");
      const langLabel = langElem.querySelector("label");
      const downloadPhase = this.downloadPhases.get(
        langLabel.getAttribute("value")
      ).downloadPhase;

      langButton.removeAttribute("disabled");

      if (
        downloadPhase !== "downloaded" &&
        allLanguageDownloadStatus === "downloaded"
      ) {
        // In case of "All languages" downloaded
        this.changeButtonState({
          langButton,
          langTag: langLabel.getAttribute("value"),
          langState: "downloaded",
        });
      } else if (
        downloadPhase === "downloaded" &&
        allLanguageDownloadStatus === "removed"
      ) {
        // In case of "All languages" removed
        this.changeButtonState({
          langButton,
          langTag: langLabel.getAttribute("value"),
          langState: "removed",
        });
      }
    }
  },

  /**
   *  Updates the state of a language download button.
   *
   * This function changes the button's appearance and behavior based on the current language state
   * (e.g., "download", "loading", or "removed"). The button's icon and CSS class are updated to reflect
   * the state, and the appropriate event handler is set for downloading or removing the language.
   * The aria-label for accessibility is also updated using the Fluent string.
   *
   * @param {object} options -
   * @param {Element} options.langButton - The HTML button element representing the language action (download/remove).
   * @param {string} options.langTag - The BCP-47 language tag for the language associated with the button.
   * @param {string} options.langState - The current state of the language, which can be "downloaded", "loading", or "removed".
   */
  changeButtonState({ langButton, langTag, langState }) {
    // Remove any icon by removing it's respective CSS class
    langButton.classList.remove(
      "translations-settings-download-icon",
      "translations-settings-loading-icon",
      "translations-settings-remove-icon"
    );
    // Set new icon based on the state of the language model
    switch (langState) {
      case "downloaded":
        // If language is downloaded show 'remove icon' as an option
        // for the user to remove the downloaded language model.
        langButton.classList.add("translations-settings-remove-icon");
        // The respective aria-label for accessibility is updated with correct Fluent string.
        if (langTag === "all") {
          document.l10n.setAttributes(
            langButton,
            "translations-settings-remove-all-button"
          );
        } else {
          document.l10n.setAttributes(
            langButton,
            "translations-settings-remove-button",
            {
              name: document.l10n.getAttributes(langButton).args.name,
            }
          );
        }
        break;
      case "removed":
        // If language is removed show 'download icon' as an option
        // for the user to download the language model.
        langButton.classList.add("translations-settings-download-icon");
        // The respective aria-label for accessibility is updated with correct Fluent string.
        if (langTag === "all") {
          document.l10n.setAttributes(
            langButton,
            "translations-settings-download-all-button"
          );
        } else {
          document.l10n.setAttributes(
            langButton,
            "translations-settings-download-button",
            {
              name: document.l10n.getAttributes(langButton).args.name,
            }
          );
        }
        break;
      case "loading":
        // While processing the download or remove language model
        // show 'loading icon' to the user
        langButton.classList.add("translations-settings-loading-icon");
        // The respective aria-label for accessibility is updated with correct Fluent string.
        if (langTag === "all") {
          document.l10n.setAttributes(
            langButton,
            "translations-settings-loading-all-button"
          );
        } else {
          document.l10n.setAttributes(
            langButton,
            "translations-settings-loading-button",
            {
              name: document.l10n.getAttributes(langButton).args.name,
            }
          );
        }
        break;
    }
    this.updateDownloadPhase(langTag, langState);
  },
};
