/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
/* 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";

/**
 * 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 showing Translations UI.
 */
const NEVER_TRANSLATE_LANGS_PREF =
  "browser.translations.neverTranslateLanguages";

function Tree(aId, aData) {
  this._data = aData;
  this._tree = document.getElementById(aId);
  this._tree.view = this;
}

Tree.prototype = {
  get tree() {
    return this._tree;
  },
  get isEmpty() {
    return !this._data.length;
  },
  get hasSelection() {
    return this.selection.count > 0;
  },
  getSelectedItems() {
    let result = [];

    let rc = this.selection.getRangeCount();
    for (let i = 0; i < rc; ++i) {
      let min = {},
        max = {};
      this.selection.getRangeAt(i, min, max);
      for (let j = min.value; j <= max.value; ++j) {
        result.push(this._data[j]);
      }
    }

    return result;
  },

  // nsITreeView implementation
  get rowCount() {
    return this._data.length;
  },
  getCellText(aRow, aColumn) {
    return this._data[aRow];
  },
  isSeparator(aIndex) {
    return false;
  },
  isSorted() {
    return false;
  },
  isContainer(aIndex) {
    return false;
  },
  setTree(aTree) {},
  getImageSrc(aRow, aColumn) {},
  getCellValue(aRow, aColumn) {},
  cycleHeader(column) {},
  getRowProperties(row) {
    return "";
  },
  getColumnProperties(column) {
    return "";
  },
  getCellProperties(row, column) {
    return "";
  },
  QueryInterface: ChromeUtils.generateQI(["nsITreeView"]),
};

function Lang(aCode, label) {
  this.langCode = aCode;
  this._label = label;
}

Lang.prototype = {
  toString() {
    return this._label;
  },
};

var gTranslationsSettings = {
  onLoad() {
    if (this._neverTranslateSiteTree) {
      // Re-using an open dialog, clear the old observers.
      this.removeObservers();
    }

    // Load site permissions into an array.
    this._neverTranslateSites = [];
    for (const perm of Services.perms.getAllByTypes([
      TRANSLATIONS_PERMISSION,
    ])) {
      if (perm.capability === Services.perms.DENY_ACTION) {
        this._neverTranslateSites.push(perm.principal.origin);
      }
    }
    let stripProtocol = s => s?.replace(/^\w+:/, "") || "";
    this._neverTranslateSites.sort((a, b) => {
      return stripProtocol(a).localeCompare(stripProtocol(b));
    });

    // Load language tags into arrays.
    this._alwaysTranslateLangs = this.getAlwaysTranslateLanguages();
    this._neverTranslateLangs = this.getNeverTranslateLanguages();

    // Add observers for relevant prefs and permissions.
    Services.obs.addObserver(this, "perm-changed");
    Services.prefs.addObserver(ALWAYS_TRANSLATE_LANGS_PREF, this);
    Services.prefs.addObserver(NEVER_TRANSLATE_LANGS_PREF, this);

    // Build trees from the arrays.
    this._alwaysTranslateLangsTree = new Tree(
      "alwaysTranslateLanguagesTree",
      this._alwaysTranslateLangs
    );
    this._neverTranslateLangsTree = new Tree(
      "neverTranslateLanguagesTree",
      this._neverTranslateLangs
    );
    this._neverTranslateSiteTree = new Tree(
      "neverTranslateSitesTree",
      this._neverTranslateSites
    );

    // Ensure the UI for each group is in the correct state.
    this.onSelectAlwaysTranslateLanguage();
    this.onSelectNeverTranslateLanguage();
    this.onSelectNeverTranslateSite();
  },

  /**
   * Retrieves the value of a char-pref splits its value into an
   * array delimited by commas.
   *
   * This is used for the translations preferences which are comma-
   * separated lists of BCP-47 language tags.
   *
   * @param {string} pref
   * @returns {Array<string>}
   */
  getLangsFromPref(pref) {
    let rawLangs = Services.prefs.getCharPref(pref);
    if (!rawLangs) {
      return [];
    }

    let langArr = rawLangs.split(",");
    let displayNames = Services.intl.getLanguageDisplayNames(
      undefined,
      langArr
    );
    let langs = langArr.map((lang, i) => new Lang(lang, displayNames[i]));
    langs.sort();

    return langs;
  },

  /**
   * Retrieves the always-translate language tags as an array.
   * @returns {Array<string>}
   */
  getAlwaysTranslateLanguages() {
    return this.getLangsFromPref(ALWAYS_TRANSLATE_LANGS_PREF);
  },

  /**
   * Retrieves the never-translate language tags as an array.
   * @returns {Array<string>}
   */
  getNeverTranslateLanguages() {
    return this.getLangsFromPref(NEVER_TRANSLATE_LANGS_PREF);
  },

  /**
   * Handles updating the UI components on pref or permission changes.
   */
  observe(aSubject, aTopic, aData) {
    if (aTopic === "perm-changed") {
      if (aData === "cleared") {
        // Permissions have been cleared
        if (!this._neverTranslateSites.length) {
          // There were no sites with permissions set, nothing to do.
          return;
        }
        // Update the tree based on the amount of permissions removed.
        let removed = this._neverTranslateSites.splice(
          0,
          this._neverTranslateSites.length
        );
        this._neverTranslateSiteTree.tree.rowCountChanged(0, -removed.length);
      } else {
        let perm = aSubject.QueryInterface(Ci.nsIPermission);
        if (perm.type != TRANSLATIONS_PERMISSION) {
          // The updated permission was not for Translations, nothing to do.
          return;
        }
        if (aData === "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.push(perm.principal.origin);
          this._neverTranslateSites.sort();
          let tree = this._neverTranslateSiteTree.tree;
          tree.rowCountChanged(0, 1);
          tree.invalidate();
        } else if (aData == "deleted") {
          let index = this._neverTranslateSites.indexOf(perm.principal.origin);
          if (index == -1) {
            // The deleted permission was not in the tree, nothing to do.
            return;
          }
          this._neverTranslateSites.splice(index, 1);
          this._neverTranslateSiteTree.tree.rowCountChanged(index, -1);
        }
      }
      // Ensure the UI updates to the changes.
      this.onSelectNeverTranslateSite();
    } else if (aTopic === "nsPref:changed") {
      switch (aData) {
        case ALWAYS_TRANSLATE_LANGS_PREF: {
          this._alwaysTranslateLangs = this.getAlwaysTranslateLanguages();

          let alwaysTranslateLangsChange =
            this._alwaysTranslateLangs.length -
            this._alwaysTranslateLangsTree.rowCount;

          this._alwaysTranslateLangsTree._data = this._alwaysTranslateLangs;
          let alwaysTranslateLangsTree = this._alwaysTranslateLangsTree.tree;

          if (alwaysTranslateLangsChange) {
            alwaysTranslateLangsTree.rowCountChanged(
              0,
              alwaysTranslateLangsChange
            );
          }

          alwaysTranslateLangsTree.invalidate();

          // Ensure the UI updates to the changes.
          this.onSelectAlwaysTranslateLanguage();
          break;
        }
        case NEVER_TRANSLATE_LANGS_PREF: {
          this._neverTranslateLangs = this.getNeverTranslateLanguages();

          let neverTranslateLangsChange =
            this._neverTranslateLangs.length -
            this._neverTranslateLangsTree.rowCount;

          this._neverTranslateLangsTree._data = this._neverTranslateLangs;
          let neverTranslateLangsTree = this._neverTranslateLangsTree.tree;

          if (neverTranslateLangsChange) {
            neverTranslateLangsTree.rowCountChanged(
              0,
              neverTranslateLangsChange
            );
          }

          neverTranslateLangsTree.invalidate();

          // Ensure the UI updates to the changes.
          this.onSelectNeverTranslateLanguage();
          break;
        }
      }
    }
  },

  /**
   * Ensures that buttons states are enabled/disabled accordingly based on the
   * content of the trees.
   *
   * The remove button should be enabled only if an item is selected.
   * The removeAll button should be enabled any time the tree has content.
   *
   * @param {Tree} aTree
   * @param {string} aIdPart
   */
  _handleButtonDisabling(aTree, aIdPart) {
    let empty = aTree.isEmpty;
    document.getElementById("removeAll" + aIdPart + "s").disabled = empty;
    document.getElementById("remove" + aIdPart).disabled =
      empty || !aTree.hasSelection;
  },

  /**
   * Updates the UI state for the always-translate languages section.
   */
  onSelectAlwaysTranslateLanguage() {
    this._handleButtonDisabling(
      this._alwaysTranslateLangsTree,
      "AlwaysTranslateLanguage"
    );
  },

  /**
   * Updates the UI state for the never-translate languages section.
   */
  onSelectNeverTranslateLanguage() {
    this._handleButtonDisabling(
      this._neverTranslateLangsTree,
      "NeverTranslateLanguage"
    );
  },

  /**
   * Updates the UI state for the never-translate sites section.
   */
  onSelectNeverTranslateSite() {
    this._handleButtonDisabling(
      this._neverTranslateSiteTree,
      "NeverTranslateSite"
    );
  },

  /**
   * Updates the value of a language pref to match when a language is removed
   * through the UI.
   *
   * @param {string} pref
   * @param {Tree} tree
   */
  _onRemoveLanguage(pref, tree) {
    let langs = Services.prefs.getCharPref(pref);
    if (!langs) {
      return;
    }

    let removed = tree.getSelectedItems().map(l => l.langCode);

    langs = langs.split(",").filter(l => !removed.includes(l));
    Services.prefs.setCharPref(pref, langs.join(","));
  },

  /**
   * Updates the never-translate language pref when a never-translate language
   * is removed via the UI.
   */
  onRemoveAlwaysTranslateLanguage() {
    this._onRemoveLanguage(
      ALWAYS_TRANSLATE_LANGS_PREF,
      this._alwaysTranslateLangsTree
    );
  },

  /**
   * Updates the always-translate language pref when a always-translate language
   * is removed via the UI.
   */
  onRemoveNeverTranslateLanguage() {
    this._onRemoveLanguage(
      NEVER_TRANSLATE_LANGS_PREF,
      this._neverTranslateLangsTree
    );
  },

  /**
   * Updates the permissions for a never-translate site when it is removed via the UI.
   */
  onRemoveNeverTranslateSite() {
    let removedNeverTranslateSites =
      this._neverTranslateSiteTree.getSelectedItems();
    for (let origin of removedNeverTranslateSites) {
      let principal =
        Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
      Services.perms.removeFromPrincipal(principal, TRANSLATIONS_PERMISSION);
    }
  },

  /**
   * Clears the always-translate languages pref when the list is cleared in the UI.
   */
  onRemoveAllAlwaysTranslateLanguages() {
    Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "");
  },

  /**
   * Clears the never-translate languages pref when the list is cleared in the UI.
   */
  onRemoveAllNeverTranslateLanguages() {
    Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "");
  },

  /**
   * Clears the never-translate sites pref when the list is cleared in the UI.
   */
  onRemoveAllNeverTranslateSites() {
    if (this._neverTranslateSiteTree.isEmpty) {
      return;
    }

    let removedNeverTranslateSites = this._neverTranslateSites.splice(
      0,
      this._neverTranslateSites.length
    );
    this._neverTranslateSiteTree.tree.rowCountChanged(
      0,
      -removedNeverTranslateSites.length
    );

    for (let origin of removedNeverTranslateSites) {
      let principal =
        Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
      Services.perms.removeFromPrincipal(principal, TRANSLATIONS_PERMISSION);
    }

    this.onSelectNeverTranslateSite();
  },

  /**
   * Handles removing a selected always-translate language via the keyboard.
   */
  onAlwaysTranslateLanguageKeyPress(aEvent) {
    if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) {
      this.onRemoveAlwaysTranslateLanguage();
    }
  },

  /**
   * Handles removing a selected never-translate language via the keyboard.
   */
  onNeverTranslateLanguageKeyPress(aEvent) {
    if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) {
      this.onRemoveNeverTranslateLanguage();
    }
  },

  /**
   * Handles removing a selected never-translate site via the keyboard.
   */
  onNeverTranslateSiteKeyPress(aEvent) {
    if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) {
      this.onRemoveNeverTranslateSite();
    }
  },

  /**
   * Removes any active preference and permissions observers.
   */
  removeObservers() {
    Services.obs.removeObserver(this, "perm-changed");
    Services.prefs.removeObserver(ALWAYS_TRANSLATE_LANGS_PREF, this);
    Services.prefs.removeObserver(NEVER_TRANSLATE_LANGS_PREF, this);
  },
};
