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

const {Cu, Cc, Ci} = require("chrome");
const Services = require("Services");
const promise = require("promise");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CustomizeMode", "resource:///modules/CustomizeMode.jsm");
const kDeveditionChangedNotification = "devedition-theme-state-changed";
const DEVEDITION_THEME_PREF = "browser.devedition.theme.enabled";

exports.OptionsPanel = OptionsPanel;

XPCOMUtils.defineLazyGetter(this, "l10n", function() {
  let bundle = Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties");
  let l10n = function(aName, ...aArgs) {
    try {
      if (aArgs.length == 0) {
        return bundle.GetStringFromName(aName);
      } else {
        return bundle.formatStringFromName(aName, aArgs, aArgs.length);
      }
    } catch (ex) {
      Services.console.logStringMessage("Error reading '" + aName + "'");
    }
  };
  return l10n;
});

function GetPref(name) {
  let type = Services.prefs.getPrefType(name);
  switch (type) {
    case Services.prefs.PREF_STRING:
      return Services.prefs.getCharPref(name);
    case Services.prefs.PREF_INT:
      return Services.prefs.getIntPref(name);
    case Services.prefs.PREF_BOOL:
      return Services.prefs.getBoolPref(name);
    default:
      throw new Error("Unknown type");
  }
}

function SetPref(name, value) {
  let type = Services.prefs.getPrefType(name);
  switch (type) {
    case Services.prefs.PREF_STRING:
      return Services.prefs.setCharPref(name, value);
    case Services.prefs.PREF_INT:
      return Services.prefs.setIntPref(name, value);
    case Services.prefs.PREF_BOOL:
      return Services.prefs.setBoolPref(name, value);
    default:
      throw new Error("Unknown type");
  }
}

function InfallibleGetBoolPref(key) {
  try {
    return Services.prefs.getBoolPref(key);
  } catch (ex) {
    return true;
  }
}


/**
 * Represents the Options Panel in the Toolbox.
 */
function OptionsPanel(iframeWindow, toolbox) {
  this.panelDoc = iframeWindow.document;
  this.panelWin = iframeWindow;

  this.toolbox = toolbox;
  this.isReady = false;

  this._prefChanged = this._prefChanged.bind(this);
  this._themeRegistered = this._themeRegistered.bind(this);
  this._themeUnregistered = this._themeUnregistered.bind(this);
  this._disableJSClicked = this._disableJSClicked.bind(this);

  this.disableJSNode = this.panelDoc.getElementById("devtools-disable-javascript");

  this._addListeners();

  Services.obs.addObserver(this, kDeveditionChangedNotification, false);
  const EventEmitter = require("devtools/toolkit/event-emitter");
  EventEmitter.decorate(this);
}

OptionsPanel.prototype = {

  get target() {
    return this.toolbox.target;
  },

  open: function() {
    let targetPromise;

    // For local debugging we need to make the target remote.
    if (!this.target.isRemote) {
      targetPromise = this.target.makeRemote();
    } else {
      targetPromise = promise.resolve(this.target);
    }

    return targetPromise.then(() => {
      this.setupToolsList();
      this.setupToolbarButtonsList();
      this.setupThemeList();
      this.setupBrowserThemeButton();
      this.populatePreferences();
      this.updateDefaultTheme();
    }).then(() => {
      this.isReady = true;
      this.emit("ready");
      return this;
    }).then(null, function onError(aReason) {
      Cu.reportError("OptionsPanel open failed. " +
                     aReason.error + ": " + aReason.message);
    });
  },

  _addListeners: function() {
    gDevTools.on("pref-changed", this._prefChanged);
    gDevTools.on("theme-registered", this._themeRegistered);
    gDevTools.on("theme-unregistered", this._themeUnregistered);
  },

  _removeListeners: function() {
    gDevTools.off("pref-changed", this._prefChanged);
    gDevTools.off("theme-registered", this._themeRegistered);
    gDevTools.off("theme-unregistered", this._themeUnregistered);
  },

  _prefChanged: function(event, data) {
    if (data.pref === "devtools.cache.disabled") {
      let cacheDisabled = data.newValue;
      let cbx = this.panelDoc.getElementById("devtools-disable-cache");

      cbx.checked = cacheDisabled;
    }
    else if (data.pref === "devtools.theme") {
      this.updateCurrentTheme();
    } else if (data.pref === "browser.devedition.theme.enabled") {
      this.updateBrowserTheme();
    }
  },

  _themeRegistered: function(event, themeId) {
    this.setupThemeList();
  },

  _themeUnregistered: function(event, theme) {
    let themeBox = this.panelDoc.getElementById("devtools-theme-box");
    let themeOption = themeBox.querySelector("[value=" + theme.id + "]");

    if (themeOption) {
      themeBox.removeChild(themeOption);
    }
  },

  setupToolbarButtonsList: function() {
    let enabledToolbarButtonsBox = this.panelDoc.getElementById("enabled-toolbox-buttons-box");
    enabledToolbarButtonsBox.textContent = "";

    let toggleableButtons = this.toolbox.toolboxButtons;
    let setToolboxButtonsVisibility =
      this.toolbox.setToolboxButtonsVisibility.bind(this.toolbox);

    let onCheckboxClick = (checkbox) => {
      let toolDefinition = toggleableButtons.filter(tool => tool.id === checkbox.id)[0];
      Services.prefs.setBoolPref(toolDefinition.visibilityswitch, checkbox.checked);
      setToolboxButtonsVisibility();
    };

    let createCommandCheckbox = tool => {
      let checkbox = this.panelDoc.createElement("checkbox");
      checkbox.setAttribute("id", tool.id);
      checkbox.setAttribute("label", tool.label);
      checkbox.setAttribute("checked", InfallibleGetBoolPref(tool.visibilityswitch));
      checkbox.addEventListener("command", onCheckboxClick.bind(this, checkbox));
      return checkbox;
    };

    for (let tool of toggleableButtons) {
      if (this.toolbox.target.isMultiProcess && tool.id === "command-button-tilt") {
        continue;
      }

      enabledToolbarButtonsBox.appendChild(createCommandCheckbox(tool));
    }
  },

  setupToolsList: function() {
    let defaultToolsBox = this.panelDoc.getElementById("default-tools-box");
    let additionalToolsBox = this.panelDoc.getElementById("additional-tools-box");
    let toolsNotSupportedLabel = this.panelDoc.getElementById("tools-not-supported-label");
    let atleastOneToolNotSupported = false;

    defaultToolsBox.textContent = "";
    additionalToolsBox.textContent = "";

    let onCheckboxClick = function(id) {
      let toolDefinition = gDevTools._tools.get(id);
      // Set the kill switch pref boolean to true
      Services.prefs.setBoolPref(toolDefinition.visibilityswitch, this.checked);
      if (this.checked) {
        gDevTools.emit("tool-registered", id);
      }
      else {
        gDevTools.emit("tool-unregistered", toolDefinition);
      }
    };

    let createToolCheckbox = tool => {
      let checkbox = this.panelDoc.createElement("checkbox");
      checkbox.setAttribute("id", tool.id);
      checkbox.setAttribute("tooltiptext", tool.tooltip || "");
      if (tool.isTargetSupported(this.target)) {
        checkbox.setAttribute("label", tool.label);
      }
      else {
        atleastOneToolNotSupported = true;
        checkbox.setAttribute("label",
                              l10n("options.toolNotSupportedMarker", tool.label));
        checkbox.setAttribute("unsupported", "");
      }
      checkbox.setAttribute("checked", InfallibleGetBoolPref(tool.visibilityswitch));
      checkbox.addEventListener("command", onCheckboxClick.bind(checkbox, tool.id));
      return checkbox;
    };

    // Populating the default tools lists
    let toggleableTools = gDevTools.getDefaultTools().filter(tool => {
      return tool.visibilityswitch;
    });

    for (let tool of toggleableTools) {
      defaultToolsBox.appendChild(createToolCheckbox(tool));
    }

    // Populating the additional tools list that came from add-ons.
    let atleastOneAddon = false;
    for (let tool of gDevTools.getAdditionalTools()) {
      atleastOneAddon = true;
      additionalToolsBox.appendChild(createToolCheckbox(tool));
    }

    if (!atleastOneAddon) {
      additionalToolsBox.style.display = "none";
      additionalToolsBox.previousSibling.style.display = "none";
    }

    if (!atleastOneToolNotSupported) {
      toolsNotSupportedLabel.style.display = "none";
    }

    this.panelWin.focus();
  },

  setupThemeList: function() {
    let themeBox = this.panelDoc.getElementById("devtools-theme-box");
    themeBox.textContent = "";

    let createThemeOption = theme => {
      let radio = this.panelDoc.createElement("radio");
      radio.setAttribute("value", theme.id);
      radio.setAttribute("label", theme.label);
      return radio;
    };

    // Populating the default theme list
    let themes = gDevTools.getThemeDefinitionArray();
    for (let theme of themes) {
      themeBox.appendChild(createThemeOption(theme));
    }

    this.updateCurrentTheme();
  },

  /**
   * Similar to `populatePrefs`, except we want more
   * special rules for the browser theme button.
   */
  setupBrowserThemeButton: function() {
    let checkbox = this.panelDoc.getElementById("devtools-browser-theme");

    checkbox.addEventListener("command", function() {
      setPrefAndEmit(DEVEDITION_THEME_PREF, this.checked);
    }.bind(checkbox));

    this.updateBrowserThemeButton();
  },

  /**
   * Called on theme changed via observer of "devedition-theme-state-changed".
   */
  updateBrowserThemeButton: function() {
    let checkbox = this.panelDoc.getElementById("devtools-browser-theme");

    // Check if the dev edition style sheet is applied -- will not
    // be applied when dev edition theme is disabled, or when there's
    // a LWT applied.
    if (this._isDevEditionThemeOn()) {
      checkbox.setAttribute("checked", "true");
    } else {
      checkbox.removeAttribute("checked");
    }

    // Should the button be shown
    if (GetPref("browser.devedition.theme.showCustomizeButton")) {
      checkbox.removeAttribute("hidden");
    } else {
      checkbox.setAttribute("hidden", "true");
    }
  },

  /**
   * Called when clicking the browser theme button to enable/disable
   * the dev edition browser theme.
   */
  updateBrowserTheme: function() {
    let enabled = GetPref("browser.devedition.theme.enabled");
    CustomizeMode.prototype.toggleDevEditionTheme.call(this, enabled);
  },

  populatePreferences: function() {
    let prefCheckboxes = this.panelDoc.querySelectorAll("checkbox[data-pref]");
    for (let checkbox of prefCheckboxes) {
      checkbox.checked = GetPref(checkbox.getAttribute("data-pref"));
      checkbox.addEventListener("command", function() {
        setPrefAndEmit(this.getAttribute("data-pref"), this.checked);
      }.bind(checkbox));
    }
    let prefRadiogroups = this.panelDoc.querySelectorAll("radiogroup[data-pref]");
    for (let radiogroup of prefRadiogroups) {
      let selectedValue = GetPref(radiogroup.getAttribute("data-pref"));
      for (let radio of radiogroup.childNodes) {
        radiogroup.selectedIndex = -1;
        if (radio.getAttribute("value") == selectedValue) {
          radiogroup.selectedItem = radio;
          break;
        }
      }
      radiogroup.addEventListener("select", function() {
        setPrefAndEmit(this.getAttribute("data-pref"), this.selectedItem.getAttribute("value"));
      }.bind(radiogroup));
    }
    let prefMenulists = this.panelDoc.querySelectorAll("menulist[data-pref]");
    for (let menulist of prefMenulists) {
      let pref = GetPref(menulist.getAttribute("data-pref"));
      let menuitems = menulist.querySelectorAll("menuitem");
      for (let menuitem of menuitems) {
        let value = menuitem.value;
        if (value == pref) { // non strict check to allow int values.
          menulist.selectedItem = menuitem;
          break;
        }
      }
      menulist.addEventListener("command", function() {
        setPrefAndEmit(this.getAttribute("data-pref"), this.value);
      }.bind(menulist));
    }

    if (this.target.activeTab) {
      this.target.client.attachTab(this.target.activeTab._actor, (response) => {
        this._origJavascriptEnabled = response.javascriptEnabled;
        this.disableJSNode.checked = !this._origJavascriptEnabled
        this.disableJSNode.addEventListener("click", this._disableJSClicked, false);
      });
    } else {
      this.disableJSNode.hidden = true;
    }
  },

  updateDefaultTheme: function() {
    // Make sure a theme is set in case the previous one coming from
    // an extension isn't available anymore.
    let themeBox = this.panelDoc.getElementById("devtools-theme-box");
    if (themeBox.selectedIndex == -1) {
      themeBox.selectedItem = themeBox.querySelector("[value=light]");
    }
  },

  updateCurrentTheme: function() {
    let currentTheme = GetPref("devtools.theme");
    let themeBox = this.panelDoc.getElementById("devtools-theme-box");
    let themeOption = themeBox.querySelector("[value=" + currentTheme + "]");

    if (themeOption) {
      themeBox.selectedItem = themeOption;
    }
  },

  /**
   * Disables JavaScript for the currently loaded tab. We force a page refresh
   * here because setting docShell.allowJavascript to true fails to block JS
   * execution from event listeners added using addEventListener(), AJAX calls
   * and timers. The page refresh prevents these things from being added in the
   * first place.
   *
   * @param {Event} event
   *        The event sent by checking / unchecking the disable JS checkbox.
   */
  _disableJSClicked: function(event) {
    let checked = event.target.checked;

    let options = {
      "javascriptEnabled": !checked
    };

    this.target.activeTab.reconfigure(options);
  },

  /**
   * Returns a boolean indicating whether or not the dev edition
   * browser theme is applied.
   */
  _isDevEditionThemeOn: function() {
    let win = Services.wm.getMostRecentWindow("navigator:browser");
    return !!(win && win.DevEdition.styleSheet);
  },

  /**
   * Called on observer notification for "devedition-theme-state-changed"
   * to possibly change the state of the dev edition button
   */
  observe: function(aSubject, aTopic, aData) {
    if (aTopic === kDeveditionChangedNotification) {
      this.updateBrowserThemeButton();
    }
  },

  destroy: function() {
    if (this.destroyPromise) {
      return this.destroyPromise;
    }

    let deferred = promise.defer();

    this.destroyPromise = deferred.promise;
    this._removeListeners();

    if (this.target.activeTab) {
      this.disableJSNode.removeEventListener("click", this._disableJSClicked, false);
      // If JavaScript is disabled we need to revert it to it's original value.
      let options = {
        "javascriptEnabled": this._origJavascriptEnabled
      };
      this.target.activeTab.reconfigure(options, () => {
        this.toolbox = null;
        deferred.resolve();
      }, true);
    }

    this.panelWin = this.panelDoc = this.disableJSNode = null;

    Services.obs.removeObserver(this, kDeveditionChangedNotification);

    return deferred.promise;
  }
};

/* Set a pref and emit the pref-changed event if needed. */
function setPrefAndEmit(prefName, newValue) {
  let data = {
    pref: prefName,
    newValue: newValue
  };
  data.oldValue = GetPref(data.pref);
  SetPref(data.pref, data.newValue);

  if (data.newValue != data.oldValue) {
    gDevTools.emit("pref-changed", data);
  }
}
