/* 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";

var Cm = Components.manager;

const VKB_ENTER_KEY = 13; // User press of VKB enter key
const INITIAL_PAGE_DELAY = 500; // Initial pause on program start for scroll alignment
const PREFS_BUFFER_MAX = 30; // Max prefs buffer size for getPrefsBuffer()
const PAGE_SCROLL_TRIGGER = 200; // Triggers additional getPrefsBuffer() on user scroll-to-bottom
const FILTER_CHANGE_TRIGGER = 200; // Delay between responses to filterInput changes
const INNERHTML_VALUE_DELAY = 100; // Delay before providing prefs innerHTML value

var gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
  Ci.nsIClipboardHelper
);

/* ============================== NewPrefDialog ==============================
 *
 * New Preference Dialog Object and methods
 *
 * Implements User Interfaces for creation of a single(new) Preference setting
 *
 */
var NewPrefDialog = {
  _prefsShield: null,

  _newPrefsDialog: null,
  _newPrefItem: null,
  _prefNameInputElt: null,
  _prefTypeSelectElt: null,

  _booleanValue: null,
  _booleanToggle: null,
  _stringValue: null,
  _intValue: null,

  _positiveButton: null,

  get type() {
    return this._prefTypeSelectElt.value;
  },

  set type(aType) {
    this._prefTypeSelectElt.value = aType;
    switch (this._prefTypeSelectElt.value) {
      case "boolean":
        this._prefTypeSelectElt.selectedIndex = 0;
        break;
      case "string":
        this._prefTypeSelectElt.selectedIndex = 1;
        break;
      case "int":
        this._prefTypeSelectElt.selectedIndex = 2;
        break;
    }

    this._newPrefItem.setAttribute("typestyle", aType);
  },

  // Init the NewPrefDialog
  init: function AC_init() {
    this._prefsShield = document.getElementById("prefs-shield");

    this._newPrefsDialog = document.getElementById("new-pref-container");
    this._newPrefItem = document.getElementById("new-pref-item");
    this._prefNameInputElt = document.getElementById("new-pref-name");
    this._prefTypeSelectElt = document.getElementById("new-pref-type");

    this._booleanValue = document.getElementById("new-pref-value-boolean");
    this._stringValue = document.getElementById("new-pref-value-string");
    this._intValue = document.getElementById("new-pref-value-int");

    this._positiveButton = document.getElementById("positive-button");
  },

  // Called to update positive button to display text ("Create"/"Change), and enabled/disabled status
  // As new pref name is initially displayed, re-focused, or modifed during user input
  _updatePositiveButton: function AC_updatePositiveButton(aPrefName) {
    document.l10n.setAttributes(
      this._positiveButton,
      "config-new-pref-create-button"
    );
    this._positiveButton.setAttribute("disabled", true);
    if (aPrefName == "") {
      return;
    }

    // If item already in list, it's being changed, else added
    const item = AboutConfig._list.filter(i => {
      return i.name == aPrefName;
    });
    if (item.length) {
      document.l10n.setAttributes(
        this._positiveButton,
        "config-new-pref-change-button"
      );
    } else {
      this._positiveButton.removeAttribute("disabled");
    }
  },

  // When we want to cancel/hide an existing, or show a new pref dialog
  toggleShowHide: function AC_toggleShowHide() {
    if (this._newPrefsDialog.classList.contains("show")) {
      this.hide();
    } else {
      this._show();
    }
  },

  // When we want to show the new pref dialog / shield the prefs list
  _show: function AC_show() {
    this._newPrefsDialog.classList.add("show");
    this._prefsShield.setAttribute("shown", true);

    // Initial default field values
    this._prefNameInputElt.value = "";
    this._updatePositiveButton(this._prefNameInputElt.value);

    this.type = "boolean";
    this._booleanValue.value = "false";
    this._stringValue.value = "";
    this._intValue.value = "";

    this._prefNameInputElt.focus();

    window.addEventListener("keypress", this.handleKeypress);
  },

  // When we want to cancel/hide the new pref dialog / un-shield the prefs list
  hide: function AC_hide() {
    this._newPrefsDialog.classList.remove("show");
    this._prefsShield.removeAttribute("shown");

    window.removeEventListener("keypress", this.handleKeypress);
  },

  // Watch user key input so we can provide Enter key action, commit input values
  handleKeypress: function AC_handleKeypress(aEvent) {
    // Close our VKB on new pref enter key press
    if (aEvent.keyCode == VKB_ENTER_KEY) {
      aEvent.target.blur();
    }
  },

  // New prefs create dialog only allows creating a non-existing preference, doesn't allow for
  // Changing an existing one on-the-fly, tap existing/displayed line item pref for that
  create: function AC_create() {
    if (this._positiveButton.getAttribute("disabled") == "true") {
      return;
    }

    switch (this.type) {
      case "boolean":
        Services.prefs.setBoolPref(
          this._prefNameInputElt.value,
          !!(this._booleanValue.value == "true")
        );
        break;
      case "string":
        Services.prefs.setCharPref(
          this._prefNameInputElt.value,
          this._stringValue.value
        );
        break;
      case "int":
        Services.prefs.setIntPref(
          this._prefNameInputElt.value,
          this._intValue.value
        );
        break;
    }

    // Ensure pref adds flushed to disk immediately
    Services.prefs.savePrefFile(null);

    this.hide();
  },

  // Display proper positive button text/state on new prefs name input focus
  focusName: function AC_focusName(aEvent) {
    this._updatePositiveButton(aEvent.target.value);
  },

  // Display proper positive button text/state as user changes new prefs name
  updateName: function AC_updateName(aEvent) {
    this._updatePositiveButton(aEvent.target.value);
  },

  // In new prefs dialog, bool prefs are <input type="text">, as they aren't yet tied to an
  // Actual Services.prefs.*etBoolPref()
  toggleBoolValue: function AC_toggleBoolValue() {
    this._booleanValue.value =
      this._booleanValue.value == "true" ? "false" : "true";
  },
};

/* ============================== AboutConfig ==============================
 *
 * Main AboutConfig object and methods
 *
 * Implements User Interfaces for maintenance of a list of Preference settings
 *
 */
var AboutConfig = {
  contextMenuLINode: null,
  filterInput: null,
  _filterPrevInput: null,
  _filterChangeTimer: null,
  _prefsContainer: null,
  _loadingContainer: null,
  _list: null,

  // Init the main AboutConfig dialog
  init: function AC_init() {
    this.filterInput = document.getElementById("filter-input");
    this._prefsContainer = document.getElementById("prefs-container");
    this._loadingContainer = document.getElementById("loading-container");

    const list = Services.prefs.getChildList("");
    this._list = list.sort().map(function AC_getMapPref(aPref) {
      return new Pref(aPref);
    }, this);

    // Support filtering about:config via a ?filter=<string> param
    const match = /[?&]filter=([^&]+)/i.exec(window.location.href);
    if (match) {
      this.filterInput.value = decodeURIComponent(match[1]);
    }

    // Display the current prefs list (retains searchFilter value)
    this.bufferFilterInput();

    // Setup the prefs observers
    Services.prefs.addObserver("", this);
  },

  // Uninit the main AboutConfig dialog
  uninit: function AC_uninit() {
    // Remove the prefs observer
    Services.prefs.removeObserver("", this);
  },

  // Buffer down rapid changes in filterInput value from keyboard
  bufferFilterInput: function AC_bufferFilterInput() {
    if (this._filterChangeTimer) {
      clearTimeout(this._filterChangeTimer);
    }

    this._filterChangeTimer = setTimeout(() => {
      this._filterChangeTimer = null;
      // Display updated prefs list when filterInput value settles
      this._displayNewList();
    }, FILTER_CHANGE_TRIGGER);
  },

  // Update displayed list when filterInput value changes
  _displayNewList: function AC_displayNewList() {
    // This survives the search filter value past a page refresh
    this.filterInput.setAttribute("value", this.filterInput.value);

    // Don't start new filter search if same as last
    if (this.filterInput.value == this._filterPrevInput) {
      return;
    }
    this._filterPrevInput = this.filterInput.value;

    // Clear list item selection / context menu, prefs list, get first buffer, set scrolling on
    this.selected = "";
    this._clearPrefsContainer();
    this._addMorePrefsToContainer();
    window.onscroll = this.onScroll.bind(this);

    // Pause for screen to settle, then ensure at top
    setTimeout(() => {
      window.scrollTo(0, 0);
    }, INITIAL_PAGE_DELAY);
  },

  // Clear the displayed preferences list
  _clearPrefsContainer: function AC_clearPrefsContainer() {
    // Quick clear the prefsContainer list
    const empty = this._prefsContainer.cloneNode(false);
    this._prefsContainer.parentNode.replaceChild(empty, this._prefsContainer);
    this._prefsContainer = empty;

    // Quick clear the prefs li.HTML list
    this._list.forEach(function (item) {
      delete item.li;
    });
  },

  // Get a small manageable block of prefs items, and add them to the displayed list
  _addMorePrefsToContainer: function AC_addMorePrefsToContainer() {
    // Create filter regex
    const filterExp = this.filterInput.value
      ? new RegExp(this.filterInput.value, "i")
      : null;

    // Get a new block for the display list
    const prefsBuffer = [];
    for (
      let i = 0;
      i < this._list.length && prefsBuffer.length < PREFS_BUFFER_MAX;
      i++
    ) {
      if (!this._list[i].li && this._list[i].test(filterExp)) {
        prefsBuffer.push(this._list[i]);
      }
    }

    // Add the new block to the displayed list
    for (let i = 0; i < prefsBuffer.length; i++) {
      this._prefsContainer.appendChild(prefsBuffer[i].getOrCreateNewLINode());
    }

    // Determine if anything left to add later by scrolling
    let anotherPrefsBufferRemains = false;
    for (let i = 0; i < this._list.length; i++) {
      if (!this._list[i].li && this._list[i].test(filterExp)) {
        anotherPrefsBufferRemains = true;
        break;
      }
    }

    if (anotherPrefsBufferRemains) {
      // If still more could be displayed, show the throbber
      this._loadingContainer.style.display = "block";
    } else {
      // If no more could be displayed, hide the throbber, and stop noticing scroll events
      this._loadingContainer.style.display = "none";
      window.onscroll = null;
    }
  },

  // If scrolling at the bottom, maybe add some more entries
  onScroll: function AC_onScroll() {
    if (
      this._prefsContainer.scrollHeight -
        (window.pageYOffset + window.innerHeight) <
      PAGE_SCROLL_TRIGGER
    ) {
      if (!this._filterChangeTimer) {
        this._addMorePrefsToContainer();
      }
    }
  },

  // Return currently selected list item node
  get selected() {
    return document.querySelector(".pref-item.selected");
  },

  // Set list item node as selected
  set selected(aSelection) {
    const currentSelection = this.selected;
    if (aSelection == currentSelection) {
      return;
    }

    // Clear any previous selection
    if (currentSelection) {
      currentSelection.classList.remove("selected");
      currentSelection.removeEventListener("keypress", this.handleKeypress);
    }

    // Set any current selection
    if (aSelection) {
      aSelection.classList.add("selected");
      aSelection.addEventListener("keypress", this.handleKeypress);
    }
  },

  // Watch user key input so we can provide Enter key action, commit input values
  handleKeypress: function AC_handleKeypress(aEvent) {
    if (aEvent.keyCode == VKB_ENTER_KEY) {
      aEvent.target.blur();
    }
  },

  // Return the target list item node of an action event
  getLINodeForEvent: function AC_getLINodeForEvent(aEvent) {
    let node = aEvent.target;
    while (node && node.nodeName != "li") {
      node = node.parentNode;
    }

    return node;
  },

  // Return a pref of a list item node
  _getPrefForNode: function AC_getPrefForNode(aNode) {
    const pref = aNode.getAttribute("name");

    return new Pref(pref);
  },

  // When list item name or value are tapped
  selectOrToggleBoolPref: function AC_selectOrToggleBoolPref(aEvent) {
    const node = this.getLINodeForEvent(aEvent);

    // If not already selected, just do so
    if (this.selected != node) {
      this.selected = node;
      return;
    }

    // If already selected, and value is boolean, toggle it
    const pref = this._getPrefForNode(node);
    if (pref.type != Services.prefs.PREF_BOOL) {
      return;
    }

    this.toggleBoolPref(aEvent);
  },

  // When finalizing list input values due to blur
  setIntOrStringPref: function AC_setIntOrStringPref(aEvent) {
    const node = this.getLINodeForEvent(aEvent);

    // Skip if locked
    const pref = this._getPrefForNode(node);
    if (pref.locked) {
      return;
    }

    // Boolean inputs blur to remove focus from "button"
    if (pref.type == Services.prefs.PREF_BOOL) {
      return;
    }

    // String and Int inputs change / commit on blur
    pref.value = aEvent.target.value;
  },

  // When we reset a pref to it's default value (note resetting a user created pref will delete it)
  resetDefaultPref: function AC_resetDefaultPref(aEvent) {
    const node = this.getLINodeForEvent(aEvent);

    // If not already selected, do so
    if (this.selected != node) {
      this.selected = node;
    }

    // Reset will handle any locked condition
    const pref = this._getPrefForNode(node);
    pref.reset();

    // Ensure pref reset flushed to disk immediately
    Services.prefs.savePrefFile(null);
  },

  // When we want to toggle a bool pref
  toggleBoolPref: function AC_toggleBoolPref(aEvent) {
    const node = this.getLINodeForEvent(aEvent);

    // Skip if locked, or not boolean
    const pref = this._getPrefForNode(node);
    if (pref.locked) {
      return;
    }

    // Toggle, and blur to remove field focus
    pref.value = !pref.value;
    aEvent.target.blur();
  },

  // When Int inputs have their Up or Down arrows toggled
  incrOrDecrIntPref: function AC_incrOrDecrIntPref(aEvent, aInt) {
    const node = this.getLINodeForEvent(aEvent);

    // Skip if locked
    const pref = this._getPrefForNode(node);
    if (pref.locked) {
      return;
    }

    pref.value += aInt;
  },

  // Observe preference changes
  observe: function AC_observe(aSubject, aTopic, aPrefName) {
    const pref = new Pref(aPrefName);

    // Ignore uninteresting changes, and avoid "private" preferences
    if (aTopic != "nsPref:changed") {
      return;
    }

    // If pref type invalid, refresh display as user reset/removed an item from the list
    if (pref.type == Services.prefs.PREF_INVALID) {
      document.location.reload();
      return;
    }

    // If pref onscreen, update in place.
    const item = document.querySelector(
      '.pref-item[name="' + CSS.escape(pref.name) + '"]'
    );
    if (item) {
      item.setAttribute("value", pref.value);
      const input = item.querySelector("input");
      input.setAttribute("value", pref.value);
      input.value = pref.value;

      pref.default
        ? item.querySelector(".reset").setAttribute("disabled", "true")
        : item.querySelector(".reset").removeAttribute("disabled");
      return;
    }

    // If pref not already in list, refresh display as it's being added
    const anyWhere = this._list.filter(i => {
      return i.name == pref.name;
    });
    if (!anyWhere.length) {
      document.location.reload();
    }
  },

  // Quick context menu helpers for about:config
  clipboardCopy: function AC_clipboardCopy(aField) {
    const pref = this._getPrefForNode(this.contextMenuLINode);
    if (aField == "name") {
      gClipboardHelper.copyString(pref.name);
    } else {
      gClipboardHelper.copyString(pref.value);
    }
  },
};

/* ============================== Pref ==============================
 *
 * Individual Preference object / methods
 *
 * Defines a Pref object, a document list item tied to Preferences Services
 * And the methods by which they interact.
 *
 */
function Pref(aName) {
  this.name = aName;
}

Pref.prototype = {
  get type() {
    return Services.prefs.getPrefType(this.name);
  },

  get value() {
    switch (this.type) {
      case Services.prefs.PREF_BOOL:
        return Services.prefs.getBoolPref(this.name);
      case Services.prefs.PREF_INT:
        return Services.prefs.getIntPref(this.name);
      case Services.prefs.PREF_STRING:
      default:
        return Services.prefs.getCharPref(this.name);
    }
  },
  set value(aPrefValue) {
    switch (this.type) {
      case Services.prefs.PREF_BOOL:
        Services.prefs.setBoolPref(this.name, aPrefValue);
        break;
      case Services.prefs.PREF_INT:
        Services.prefs.setIntPref(this.name, aPrefValue);
        break;
      case Services.prefs.PREF_STRING:
      default:
        Services.prefs.setCharPref(this.name, aPrefValue);
    }

    // Ensure pref change flushed to disk immediately
    Services.prefs.savePrefFile(null);
  },

  get default() {
    return !Services.prefs.prefHasUserValue(this.name);
  },

  get locked() {
    return Services.prefs.prefIsLocked(this.name);
  },

  reset: function AC_reset() {
    Services.prefs.clearUserPref(this.name);
  },

  test: function AC_test(aValue) {
    return aValue ? aValue.test(this.name) : true;
  },

  // Get existing or create new LI node for the pref
  getOrCreateNewLINode: function AC_getOrCreateNewLINode() {
    if (!this.li) {
      this.li = document.createElement("li");

      this.li.className = "pref-item";
      this.li.setAttribute("name", this.name);

      // Click callback to ensure list item selected even on no-action tap events
      this.li.addEventListener("click", function (aEvent) {
        AboutConfig.selected = AboutConfig.getLINodeForEvent(aEvent);
      });

      // Contextmenu callback to identify selected list item
      this.li.addEventListener("contextmenu", function (aEvent) {
        AboutConfig.contextMenuLINode = AboutConfig.getLINodeForEvent(aEvent);
      });

      this.li.setAttribute("contextmenu", "prefs-context-menu");

      const prefName = document.createElement("div");
      prefName.className = "pref-name";
      prefName.addEventListener("click", function (event) {
        AboutConfig.selectOrToggleBoolPref(event);
      });
      prefName.textContent = this.name;

      this.li.appendChild(prefName);

      const prefItemLine = document.createElement("div");
      prefItemLine.className = "pref-item-line";

      const prefValue = document.createElement("input");
      prefValue.className = "pref-value";
      prefValue.addEventListener("blur", function (event) {
        AboutConfig.setIntOrStringPref(event);
      });
      prefValue.addEventListener("click", function (event) {
        AboutConfig.selectOrToggleBoolPref(event);
      });
      prefValue.value = "";
      prefItemLine.appendChild(prefValue);

      const resetButton = document.createElement("div");
      resetButton.className = "pref-button reset";
      resetButton.addEventListener("click", function (event) {
        AboutConfig.resetDefaultPref(event);
      });
      resetButton.setAttribute("data-l10n-id", "config-pref-reset-button");
      prefItemLine.appendChild(resetButton);

      const toggleButton = document.createElement("div");
      toggleButton.className = "pref-button toggle";
      toggleButton.addEventListener("click", function (event) {
        AboutConfig.toggleBoolPref(event);
      });
      toggleButton.setAttribute("data-l10n-id", "config-pref-toggle-button");
      prefItemLine.appendChild(toggleButton);

      const upButton = document.createElement("div");
      upButton.className = "pref-button up";
      upButton.addEventListener("click", function (event) {
        AboutConfig.incrOrDecrIntPref(event, 1);
      });
      prefItemLine.appendChild(upButton);

      const downButton = document.createElement("div");
      downButton.className = "pref-button down";
      downButton.addEventListener("click", function (event) {
        AboutConfig.incrOrDecrIntPref(event, -1);
      });
      prefItemLine.appendChild(downButton);

      this.li.appendChild(prefItemLine);

      // Delay providing the list item values, until the LI is returned and added to the document
      setTimeout(this._valueSetup.bind(this), INNERHTML_VALUE_DELAY);
    }

    return this.li;
  },

  // Initialize list item object values
  _valueSetup: function AC_valueSetup() {
    this.li.setAttribute("type", this.type);
    this.li.setAttribute("value", this.value);

    const valDiv = this.li.querySelector(".pref-value");
    valDiv.value = this.value;

    switch (this.type) {
      case Services.prefs.PREF_BOOL:
        valDiv.setAttribute("type", "button");
        this.li.querySelector(".up").setAttribute("disabled", true);
        this.li.querySelector(".down").setAttribute("disabled", true);
        break;
      case Services.prefs.PREF_STRING:
        valDiv.setAttribute("type", "text");
        this.li.querySelector(".up").setAttribute("disabled", true);
        this.li.querySelector(".down").setAttribute("disabled", true);
        this.li.querySelector(".toggle").setAttribute("disabled", true);
        break;
      case Services.prefs.PREF_INT:
        valDiv.setAttribute("type", "number");
        this.li.querySelector(".toggle").setAttribute("disabled", true);
        break;
    }

    this.li.setAttribute("default", this.default);
    if (this.default) {
      this.li.querySelector(".reset").setAttribute("disabled", true);
    }

    if (this.locked) {
      valDiv.setAttribute("disabled", this.locked);
      this.li.querySelector(".pref-name").setAttribute("locked", true);
    }
  },
};
