/**
 * JavaScript scope registry: for preventing variable name collisions from overlays.
 * @note: Scripts loaded into a specific scope like this will inherit from this
 *        window's scope.
 */
const scopeRegistry = {
  /**
   * The service which gets our scripts.
   */
  subscriptLoader: Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
                             .getService(Components.interfaces.mozIJSSubScriptLoader),

  /**
   * A scope cache.  Please do not touch this directly.
   */
  registeredScopes: {},

  /**
   * Get a scope object corresponding to an ID.
   *
   * @param aScopeID String identifying a scope.  Best practice:  use a uuid.
   */
  getScope: function getScope(aScopeId) {
    if (typeof this.registeredScopes[aScopeId] == "undefined")
      this.registeredScopes[aScopeId] = {uuid: aScopeId};
    return this.registeredScopes[aScopeId];
  },

  /**
   * Attach a script to a scope, identified by ID.
   *
   * @param aURL The url of the script to load.
   * @param aScopeID String identifying a scope.  Best practice:  use a uuid.
   */
  loadScriptByScope: function loadScriptByScope(aURL, aScopeId) {
    if (aScopeId) {
      var scopeObj = this.getScope(aScopeId);
      this.subscriptLoader.loadSubScript(aURL, scopeObj);
    } else {
      this.subscriptLoader.loadSubScript(aURL);
    }
  }
};

/**
 * For command events, ensure the target is properly scoped per the scope registry.
 */
const rescopeEventListener = {
  /**
   * Test a string to see if it resembles exactly one JavaScript function call.
   *
   * @param oncommand string to test.
   */
  checkFunctionName: function checkFunctionName(oncommand) {
    // Sanity check:  oncommand = "functionName(args,);"
    var closeParen = oncommand.indexOf(")");
    if (!closeParen) {
      // Doh!
      return false;
    }

    const significantChars = /[^\s;]/;
    if (oncommand.substr(closeParen + 1).search(significantChars) != -1) {
      return false;
    }

    return true;
  },

  /**
   * If the target hasn't had an attempt at rescoping, then try to rescope it.
   *
   * @param evt nsIDOMEvent with type of "command" and XUL element target.
   */
  handleEvent: function handleEvent(evt) {
    if ("commandRescoped" in evt.target && evt.target.commandRescoped)
      return;

    if (!evt.target.hasAttribute("oncommand")) {
      evt.target.commandRescoped = true;
      return;
    }

    dump("rescopeEventListener: " + evt.target.nodeName + "\n");

    const ELEMENT_NODE = Components.interfaces.nsIDOMNode.ELEMENT_NODE;

    // Get the scope ID this applies to.
    var scopeId = null;
    var scopeIdNode = evt.target;
    do {
      if (scopeIdNode.hasAttribute("scopeid"))
        break;

      scopeIdNode = scopeIdNode.parentNode;
    } while (scopeIdNode.nodeType == ELEMENT_NODE);

    if (scopeIdNode.nodeType == ELEMENT_NODE)
      scopeId = scopeIdNode.getAttribute("scopeid");

    if (!scopeId) {
      // Just mark it as having been scoped and move on.
      evt.target.commandRescoped = true;
      return;
    }

    // Test the oncommand event handler.
    var oncommand = evt.target.getAttribute("oncommand");
    oncommand = oncommand.substr(oncommand.search(/[^\s]/));
    if (!this.checkFunctionName(oncommand)) {
      // D'oh!
      warn("oncommand event handler should be a single function for node: " + evt.target.id);
      evt.target.commandRescoped = true;
      return;
    }

    // Rescope the oncommand event handler.
    oncommand = "scopeRegistry.getScope('" + scopeId + "')." + oncommand;
    evt.target.setAttribute("oncommand", oncommand);
    evt.target.commandRescoped = true;
  }
};
window.addEventListener("command", rescopeEventListener, true);
