/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Verbosio XML Editor.
 *
 * The Initial Developer of the Original Code is
 * Alexander J. Vincent <ajvincent@gmail.com>.
 * Portions created by the Initial Developer are Copyright (C) 2005-2007
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

var C_i = Components.interfaces;

const nsIScriptError              = C_i.nsIScriptError;
const nsIConsoleService           = C_i.nsIConsoleService;
const xeIVerbosioUtilsService     = C_i.xeIVerbosioUtilsService;

/**
 * Wrap a JavaScript object for passing to components code.
 */
function ObjectWrapper(object) {
  this.wrappedJSObject = object;
}

/**
 * Check an object to see if it is an instance of an interface.
 *
 * @param JSObject aObject Object to check.
 * @param JSString aInterface Name of the interface within Components.interfaces.
 *
 * @return JSNumber 2 if the instanceof operator succeeded. (Truly a component)
 * @return JSNumber 1 if the QueryInterface method call succeeded. (Imitation)
 * @return JSNumber 0 otherwise.
 */
function isInstance(aObject, aInterface) {
  if ((typeof aObject != "object") || (aObject === null))
    return 0;

  var iface = Components.interfaces[aInterface];
  var obj;

  try {
    obj = (aObject instanceof iface);
    if (obj)
      return 2;
  }
  catch (e) {
    // fall through deliberately
  }

  try {
    if (typeof aObject.QueryInterface != "function")
      return 0;
    obj = (aObject.QueryInterface(iface));
    if (obj === aObject)
      return 1;
  }
  catch (e) {
    // fall through deliberately
  }

  return 0;
}

/**
 * Return an array of descendant child indexes to the user.
 *
 * @param aNode nsIDOMNode which we want the array for.
 *
 * @return JSArray of JSNumber values
 *
 * @note node(k + 1) = node(k).childNodes.item(response[k + 1])
 *       node(0) instanceof nsIDOMDocument
 */
function getDescendantIndexes(aNode) {
  var responseReversed = [];
  var currentNode = aNode;
  var parentNode = currentNode.parentNode;
  var indexNode;
  var index;

  while (parentNode) {
    indexNode = parentNode.firstChild;
    index = 0;
    while (indexNode != currentNode) {
      indexNode = indexNode.nextSibling;
      index++;
    }
    responseReversed[responseReversed.length] = index;
    currentNode = parentNode;
    parentNode = parentNode.parentNode;
  }

  return responseReversed.reverse();
}

/**
 * Get an element by its ancestor-or-self scope id and its own local id.
 *
 * @param aScopeId The scope id for the element (or for an ancestor).
 * @param aLocalId The local id for the element.
 *
 * @return nsIDOMElement matching the ID key.
 */
document.getElementByAttrKey = function getElementByAttrKey(aScopeId, aLocalId)
{
  const idPrefix = "urn:verbosio:81b072ff-b308-4040-9be3-2a4cba54420b:";
  var id = idPrefix + aScopeId + ":" + aLocalId;
  return document.getElementById(id);
};

/**
 * Global variable available for overlays to load scripts under their scope.
 */
var loadScripts = undefined;

function setVariableById(aNode) {
  window[aNode] = document.getElementById(aNode);
}
setVariableById("documentViewer");
setVariableById("utilityFrame");
setVariableById("preconditionUIBox");
setVariableById("dumpTextbox");
setVariableById("executeText");
setVariableById("verbosioLocaleStrings");
setVariableById("documentsMenuPopup");
setVariableById("viewersMenuPopup");
setVariableById = undefined;

/**
 * Overlay an element missing an ID attribute with a node.
 *
 * @param aElementId ID of one of the target element's ancestors.
 * @param aXPath XPath from the ancestor to the target element.
 * @param aResolver Namespace resolver for the target element.  May be null.
 * @param aNodeToInsert Node the user wishes to insert for the overlay.
 */
function overlayElementByXPath(aElementId, aXPath, aResolver, aNodeToInsert) {
  var element = document.getElementById(aElementId);
  if (!element) {
    throw Components.results.NS_ERROR_INVALID_POINTER;
  }

  const nsIXPathResult = Components.interfaces.nsIXPathResult;
  const UNORDERED_NODE = nsIXPathResult.ANY_UNORDERED_NODE_TYPE;
  var resolver = aResolver || document.createNSResolver(element);

  var xpathExpression = document.createExpression(aXPath, aResolver);
  var xpathResult = xpathExpression.evaluate(element, UNORDERED_NODE, null);
  var targetNode = xpathResult.singleNodeValue;
  targetNode.appendChild(aNodeToInsert);
}

var gDocumentPack = null;
var gTxnControl = null;
var gSelectedFiles = [];

const verbosio = {
  /* This will be changed in a load event listener to "ready" */
  loadStatus: "uninitialized",

  /* Defined in chrome://verbosio/content/verbosio.dtd */
  localURICollection: null,

  /**
   * virtual: file system service (a reduced documentRegistry)
   */
  virtualFileService: Components.classes["@verbosio.mozdev.org/virtual-file-service;1"]
                                .getService(Components.interfaces.xeIVirtualFileService),

  /**
   * The Verbosio utilities service.
   */
  utils: Components.classes["@verbosio.mozdev.org/verbosio-utils;1"]
                   .getService(xeIVerbosioUtilsService),

  // A cache of overlays to load for configuration by preferences.
  overlaysToLoad: [],

  /**
   * Load the overlays queued up by configuration.
   */
  loadOverlays: function loadOverlays() {
    for (var i = 0; i < this.overlaysToLoad.length; i++) {
      document.loadOverlay(overlaysToLoad[i]);
    }
  },

  _extensionDataFiles: null,
  get extensionDataFiles() {
    if (!this._extensionDataFiles) {
      this._extensionDataFiles = this.utils.getExtensionDataFiles({});
    }
    return this._extensionDataFiles;
  },

  /**
   * A NodeFilter for elements with a localId attribute.
   */
  hasAttrFilter: {
    FILTER_ACCEPT: Components.interfaces.nsIDOMNodeFilter.FILTER_ACCEPT,
    FILTER_SKIP: Components.interfaces.nsIDOMNodeFilter.FILTER_SKIP,

    attrName: "",

    /**
     *
     * @param aNode Element to check.
     *
     * @return NodeFilter.FILTER_ACCEPT if element has localId attribute.
     * @return NodeFilter.FILTER_SKIP   if element does not have localId attribute.
     */
    acceptNode: function acceptNode(aNode) {
      if (aNode.hasAttribute(this.attrName))
        return this.FILTER_ACCEPT;

      return this.FILTER_SKIP;
    }
  },

  /**
   * Set an ID attribute to a node, based on scope and local id.
   *
   * @param aNode  nsIDOMElement seeking ID attribute.
   * @param aScope JSString of the scope id.
   */
  idAttrSet: function idAttrSet(aNode, aScope) {
    // UUID!  Very unlikely anyone has this ID unless it's deliberate.
    var idPrefix = "urn:verbosio:81b072ff-b308-4040-9be3-2a4cba54420b:";
    dump("idPostfix: " + aScope + ":" + aNode.getAttribute("localid") + "\n");
    idPrefix += aScope + ":" + aNode.getAttribute("localid");
    aNode.setAttribute("id", idPrefix);
  },

  /**
   * Set ID ref attributes, separated by spaces and alternating between name/value.
   *
   * @param aNode  nsIDOMElement seeking ID attribute.
   * @param aScope JSString of the scope id.
   */
  idRefAttrSet: function idRefAttrSet(aNode, aScope) {
    // UUID!  Very unlikely anyone has this ID unless it's deliberate.
    var idPrefix = "urn:verbosio:81b072ff-b308-4040-9be3-2a4cba54420b:";
    idPrefix += aScope + ":";
    var localIdRefs = aNode.getAttribute("localidrefs").split(" ");
    for (var i = 0; i < localIdRefs.length; i += 2) {
      var attrName = localIdRefs[i];
      var attrValue = localIdRefs[i + 1];
      dump(attrName + "postfix: " + aScope + ":" + attrValue + "\n");
      aNode.setAttribute(attrName, idPrefix + attrValue);
    }
  },

  /**
   * Compile dynamic ID and ID ref attributes for the entire application.
   */
  compileIdsAndRefs: function compileIdsAndRefs() {
    var walker;
    walker = document.createTreeWalker(document, NodeFilter.SHOW_ELEMENT, this.hasAttrFilter, true);
    this.hasAttrFilter.attrName = "scopeid";
    var haveScopeAttrs = [];
    while (walker.nextNode()) {
      haveScopeAttrs[haveScopeAttrs.length] = walker.currentNode;
    }
    for (var i = 0; i < haveScopeAttrs.length; i++) {
      var rootNode = haveScopeAttrs[i];
      var scope = rootNode.getAttribute("scopeid");
      this.hasAttrFilter.attrName = "localid";
      walker = document.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT, this.hasAttrFilter, true);
      if (this.hasAttrFilter.acceptNode(walker.currentNode) == this.hasAttrFilter.FILTER_ACCEPT)
        this.idAttrSet(walker.currentNode, scope);
      while (walker.nextNode())
        this.idAttrSet(walker.currentNode, scope);

      walker.currentNode = rootNode;
      this.hasAttrFilter.attrName = "localidrefs";

      if (this.hasAttrFilter.acceptNode(walker.currentNode) == this.hasAttrFilter.FILTER_ACCEPT)
        this.idRefAttrSet(walker.currentNode, scope);
      while (walker.nextNode())
        this.idRefAttrSet(walker.currentNode, scope);
    }
  },

  /**
   * Get a map of namespace prefixes applicable to a particular element.
   *
   * @param aElement nsIDOMElement descendant of master document, to search.
   *
   * @return JSObject with prefixes as property names and namespace URI's as properties.
   */
  getNamespacesMap:
  function getNamespacesMap(aElement) {
    const nsIDOMNode = Components.interfaces.nsIDOMNode;
    const nsIDOMElement = Components.interfaces.nsIDOMElement;
    assert(aElement instanceof nsIDOMElement, "Pass an element to this function!");

    var node = aElement;
    var attrMap = {};
    while (node && node.nodeType == nsIDOMNode.ELEMENT_NODE) {
      var attrs = node.attributes;
      for (var i = 0; i < attrs.length; i++) {
        var attr = attrs.item(i);
        if (attr.namespaceURI != XMLNS_NS)
          continue;
        if (attr.localName in attrMap)
          continue;
        attrMap[attr.localName] = attr.nodeValue;
      }
      node = node.parentNode;
    }
    return attrMap;
  },

  advanceTxn: function advanceTxn(aTransaction) {
    assert(!aTransaction ||
           isInstance(aTransaction, "xeITransaction") > 0, "Not a transaction?");
    if (arguments.length > 1) {
      dumpJSStack();
      warn("verbosio.advanceTxn shouldn't be called with a wrapper argument.");
    }
    if (gDocumentPack.pendingAction && !gDocumentPack.pendingAction.merge(aTransaction)) {
      gDocumentPack.txmgr.doTransaction(gDocumentPack.pendingAction);
      gDocumentPack.pendingAction = null;
    }

    if (!gDocumentPack.pendingAction) {
      gDocumentPack.pendingAction = aTransaction;
    }

    baseEditController.updateCommands();
    baseGlobalController.updateCommands();
  },

  /**
   * The node editor last known in use.  Set for context menus.
   */
  nodeEditor: null,

  addWrapper: function addWrapper(wrapper) {
    const xeIDocumentWrapper = Components.interfaces.xeIDocumentWrapper;
    assert(wrapper instanceof xeIDocumentWrapper, "Not a wrapper?");
    assert(wrapper.wrappedSource, "Failed to load the document?");

    if (wrapper instanceof Components.interfaces.xeIXMLDocumentWrapper) {
      EventRecorder.addToWrapper(wrapper);
      mutationEventRegistry.registerWrapper(wrapper);
    }

    documentViewer.appendDocTab(wrapper);
    assert(documentViewer.viewTabs.childNodes.length > 0, "No viewer tabs?");

    // Add the tabs.
    var tab = documentViewer.viewTabs.firstChild;
    var viewer;
    while (tab) {
      viewer = documentViewer.getViewer(wrapper, tab, true);
      if (viewer) {
        viewer.initializeView();
      }
      tab = tab.nextSibling;
    }

    tab = documentViewer.viewTabs.selectedItem;
    if (!tab) {
      documentViewer.viewTabs.selectedIndex = 0;
      tab = documentViewer.viewTabs.selectedItem;
    }
    assert(tab, "No tab selected?!?");
    viewer = documentViewer.getViewer(wrapper, tab, false);

    var cNodes = documentViewer.publicURITabs.childNodes;
    documentViewer.selectDocTab(cNodes.length - 1);
  },

  removeWrapper: function removeWrapper(wrapper) {
    const xeIDocumentWrapper = Components.interfaces.xeIDocumentWrapper;
    assert(wrapper instanceof xeIDocumentWrapper, "Not a wrapper?");

    var tab = documentViewer.publicURITabs.firstChild;
    var tabIndex = 0;
    while (tab.getUserData("documentWrapper") != wrapper) {
      tab = tab.nextSibling;
      tabIndex++;
    }

    // Remove the tabs.
    do {
      documentViewer.removeDocTab(tabIndex);
      tab = documentViewer.publicURITabs.childNodes[tabIndex];

      if (tab == documentViewer.defaultURITab) {
        documentViewer.selectedPanel = null;
        break;
      }
    } while (tab && tab.getUserData("documentWrapper") == null);

    if (isInstance(gDocumentPack, "xeIDocumentPack")) {
      gDocumentPack.removeWrapper(wrapper);
    }

    if (wrapper instanceof Components.interfaces.xeIXMLDocumentWrapper) {
      mutationEventRegistry.unregisterWrapper(wrapper);
      EventRecorder.removeFromWrapper(wrapper);
    }
  },

  // XXX ajvincent Associate this with a Verbosio service, in case the pack's already open.
  openDocumentPack: function openDocumentPack(aPath) {
    var packTypeEntry = null;
    var path = null;

    if (!aPath) {
      const nsIFilePicker = Components.interfaces.nsIFilePicker;
      var fp = Components.classes["@mozilla.org/filepicker;1"]
                         .createInstance(nsIFilePicker);
      fp.init(window, "Open a document", nsIFilePicker.modeOpen);
      // XXX ajvincent We shouldn't need to access the data field directly.
      assert(packTypesRegistry.data.length > 0, "Need entries in pack types registry");
      for (var i = 0; i < packTypesRegistry.data.length; i++)
      {
        var typeEntry = packTypesRegistry.data[i];
        fp.appendFilter(typeEntry.title, typeEntry.filter);
      }
      var fpResult = fp.show();

      if (fpResult != nsIFilePicker.returnOK) {
        return;
      }

      path = fp.file.path;
      packTypeEntry = packTypesRegistry.data[fp.filterIndex];
    } else {
      path = aPath;
      var myFilter = "*" + path.substr(path.lastIndexOf("."));
      packTypeEntry = packTypesRegistry.searchFirstEntry({
        filter: myFilter
      });
      if (!packTypeEntry) {
        throw new Error("No pack type entry for '" + myFilter + "'");
      }
    }

    if (gDocumentPack) {
      window.openDialog("chrome://verbosio/content/verbosio.xul",
                        null,
                        "chrome",
                        path);
      return;
    }

    gDocumentPack = packTypeEntry.getExistingPack(path);

    /* If we've gotten here without an exception, we have a document pack.
       Now we need to add pack viewers.
     */
    var scopeCount = {};
    var scopes = gDocumentPack.getViewerScopes(scopeCount);
    scopeCount = scopeCount.value;

    for (var i = 0; i < scopeCount; i++) {
      var scopeObj = scopeRegistry.getScope(scopes[i]);
      var viewer = scopeObj.getViewer();

      documentViewer.insertDocPackTab(viewer);
    }

    documentViewer.selectDocTab(0);

    namespaceRegistry.init();
  },

  closeWindow: function closeWindow(evt) {
    if (evt.target != window) {
      return;
    }

    if (gDocumentPack) {
      gDocumentPack = null;
    }
  },

  // XXX ajvincent Associate this with a Verbosio service, in case the file's already open.
  openDocument: function openDocument(aPath)
  {
    var fileTypeEntry = null;
    var path = null;
    const xeIDocumentPack = Components.interfaces.xeIDocumentPack;
    assert(aPath, "No file path?");
    assert(gDocumentPack instanceof xeIDocumentPack, "Not a document pack?");

    var wrapper = gDocumentPack.getWrapper(aPath);
    dump("wrapper: " + wrapper + "\n");

    const xeIXMLDocumentWrapper = Components.interfaces.xeIXMLDocumentWrapper;

    // If the wrapper has already been opened, bail out.
    var tab = documentViewer.publicURITabs.firstChild;
    var tabIndex = 0;
    while (tab) {
      dump("tab's wrapper: " + tab.getUserData("documentWrapper") + "\n");
      if (tab.getUserData("documentWrapper") == wrapper) {
        documentViewer.selectDocTab(tabIndex);
        return;
      }
      tabIndex++;
      tab = tab.nextSibling;
    }

    this.addWrapper(wrapper);
  },

  // XXX ajvincent Associate this with a Verbosio service.
  /**
   * Save a document.
   *
   * @param aWrapper xeIDocumentWrapper which owns the document.
   *
   * @note If aWrapper is null, use the document the user is looking at.
   */
  saveDocument: function saveDocument(aWrapper) {
    if (!aWrapper) {
      var tab = documentViewer.publicURITabs.selectedItem;
      var wrapper = tab.getUserData("documentWrapper");
    } else {
      const xeIDocumentWrapper = Components.interfaces.xeIDocumentWrapper;
      assert(aWrapper instanceof xeIDocumentWrapper, "Not a document wrapper!");
      wrapper = aWrapper;
    }

    wrapper.save(null);
  },

  // XXX ajvincent Associate this with a Verbosio service.
  /**
   * Close a document.
   *
   * @param aWrapper xeIDocumentWrapper which owns the document.
   *
   * @note If aWrapper is null, use the document the user is looking at.
   */
  closeDocument: function closeDocument(aWrapper) {
    var wrapper;

    if (!aWrapper) {
      var tab = documentViewer.publicURITabs.selectedItem;
      wrapper = tab.getUserData("documentWrapper");
    } else {
      assert(aWrapper instanceof xeIDocumentWrapper, "Not a document wrapper!");
      wrapper = aWrapper;
    }

    if (wrapper.pendingAction &&
        (wrapper.pendingAction.ownerWrapper == wrapper)) {
      this.advanceTxn(wrapper.pendingAction, wrapper);
      this.advanceTxn(null, wrapper);
    }

    var index;

    if (gDocumentPack.txmgr.numberOfUndoItems != 0) {
      const psClass = "@mozilla.org/embedcomp/prompt-service;1";
      const nsIPromptService = Components.interfaces.nsIPromptService;
      var promptService = Components.classes[psClass]
                                    .getService(nsIPromptService);
      const flags =
      nsIPromptService.BUTTON_TITLE_SAVE      * nsIPromptService.BUTTON_POS_0 +
      nsIPromptService.BUTTON_TITLE_DONT_SAVE * nsIPromptService.BUTTON_POS_1 +
      nsIPromptService.BUTTON_TITLE_CANCEL    * nsIPromptService.BUTTON_POS_2 +
      nsIPromptService.BUTTON_POS_0_DEFAULT;

      var msg = wrapper.publicURI + " has unsaved changes.  Do you want to save them?";
      index = promptService.confirmEx(window, "Unsaved document", msg,
                                      flags, null, null, null, null, {});

    } else {
      index = 1; // Don't save.
    }

    switch (index) {
      case 2: // User clicked "Cancel".
        return;
      case 0: // User clicked "Save".
        this.saveDocument(wrapper);
        // Fall through.
      case 1: // User clicked "Don't save".
        this.removeWrapper(wrapper);
        break;

      default:
        throw new Error("Unexpected dialog status!");
    }
  },

  toString: function toString() {
    return "[object VerbosioEditor]";
  }
};

const txnDispatcher = {
  getListeners: function getListeners() {
    var listeners = [];
    var i;
    var wrapper = documentViewer.currentWrapper;

    listeners[0] = gDocumentPack.txlistener;

    if (isInstance(wrapper, "xeIXMLDocumentWrapper")) {
      const xmlPackClass = "@verbosio.mozdev.org/language-pack-service;1?contentType=application/xml";
      const nsITransactionListener = Components.interfaces.nsITransactionListener;
      const nsIDOMNodeFilter = Components.interfaces.nsIDOMNodeFilter;
      listeners[listeners.length] = Components.classes[xmlPackClass]
                                              .getService(nsITransactionListener);

      // Get the language packs we need.
      var docListeners = {};
      var doc = wrapper.wrappedDocument;
      var rootAncestor = wrapper.selectionRange.commonAncestorContainer;

      // Start with the descendants of the top selected node.
      var walker = doc.createTreeWalker(rootAncestor,
                                        nsIDOMNodeFilter.SHOW_ELEMENT,
                                        null,
                                        true);
      const packContractPrefix = "@verbosio.mozdev.org/language-pack-service;1?namespaceURI=";
      var packContract;
      var service;
      while (walker.nextNode()) {
        packContract = packContractPrefix + walker.currentNode.namespaceURI;
        if ((packContract in Components.classes) &&
            (!(packContract in docListeners))) {
          try {
            service = Components.classes[packContract]
                                .getService(nsISupports)
                                .QueryInterface(nsITransactionListener);

          } catch (e) {
            service = null;
          }
          docListeners[packContract] = service;
        }
      }

      // Next, look up language packs for the common ancestor and its ancestors.
      const ELEMENT_NODE = Components.interfaces.nsIDOMNode.ELEMENT_NODE;
      while (rootAncestor) {
        if (rootAncestor.nodeType == ELEMENT_NODE) {
          packContract = packContractPrefix + rootAncestor.namespaceURI;
          if ((packContract in Components.classes) &&
              (!(packContract in docListeners))) {
            service = Components.classes[packContract]
                                .getService(nsITransactionListener);
            docListeners[packContract] = service;
          }
        }

        rootAncestor = rootAncestor.parentNode;
      }

      // Add the language packs to the listener array.
      for (var prop in docListeners) {
        listeners[listeners.length] = docListeners[prop];
      }
    }

    for (i = 0; i < listeners.length; i++) {
      dump("listeners[" + i + "]: " + listeners[i] + "\n");
    }

    return listeners;
  },

  executeListeners: function executeListeners(aTransaction, aWillDo, aDoFunction, aDidDo) {
    var listeners = this.getListeners();
    var txmgr = gDocumentPack.txmgr;
    var i;

    for (i = 0; i < listeners.length; i++) {
      if (!listeners[i]) {
        continue;
      }

      try {
        var interrupt = listeners[i][aWillDo](txmgr, aTransaction);
      } catch (e) {
        dump("listeners[" + i + "]:" + listeners[i] + "\n\n");
        if ("stack" in e) {
          dump(e.stack + "\n\n");
        }
        throw e;
      }
      if (interrupt) {
        throw Components.results.NS_ERROR_ABORT;
      }
    }

    try {
      var rv = Components.results.NS_OK;
      txmgr[aDoFunction](aTransaction);
    }
    catch (e) {
      rv = e;
    }
    dump("After " + aDoFunction + ", rv = " + rv + "\n");

    for (i = listeners.length - 1; i >= 0; i--) {
      if (!listeners[i]) {
        continue;
      }

      listeners[i][aDidDo](txmgr, aTransaction, rv);
    }

    baseEditController.updateCommands();
  },

  doTransaction: function doTransaction(aTransaction) {
    this.executeListeners(aTransaction, "willDo", "doTransaction", "didDo");
  },

  undoTransaction: function undoTransaction(aTransaction) {
    this.executeListeners(aTransaction, "willUndo", "undoTransaction", "didUndo");
  },

  redoTransaction: function redoTransaction(aTransaction) {
    this.executeListeners(aTransaction, "willRedo", "redoTransaction", "didRedo");
  }
}

const baseGlobalController = {
  // nsIController
  supportsCommand: function supportsCommand(aCommandName) {
    switch (aCommandName) {
      case "cmd_open_files":
      case "cmd_open_pack":
      case "cmd_save":
      case "cmd_close":
        return true;
    }

    return false;
  },

  // nsIController
  isCommandEnabled: function isCommandEnabled(aCommandName) {
    if (aCommandName == "cmd_open_pack") {
      return true;
    }
    if (aCommandName == "cmd_open_files") {
      return gSelectedFiles.length > 0;
    }

    var wrapper = documentViewer.currentWrapper;
    if (!wrapper) {
      return false;
    }

    switch (aCommandName) {
      case "cmd_save":
        return Boolean(gDocumentPack.txmgr.numberOfUndoItems) ||
               gDocumentPack.pendingAction;

      case "cmd_close":
        return (documentViewer.publicURITabs.selectedItem !=
                documentViewer.defaultURITab);
    }

    return false;
  },

  // nsIController
  doCommand: function doCommand(aCommandName) {
    dump("baseGlobalController.doCommand called with command " + aCommandName + "\n");
    dumpJSStack();
    var wrapper = documentViewer.currentWrapper;
    var txn = null;

    try {
      preconditionUIBox.precondition = null;
      switch (aCommandName) {
        case "cmd_open_pack":
          verbosio.openDocumentPack();
          break;

        case "cmd_open_files":
          for (var i = 0; i < gSelectedFiles.length; i++) {
            verbosio.openDocument(gSelectedFiles[i]);
          }
          break;

        case "cmd_save":
          verbosio.saveDocument(null);
          break;

        case "cmd_close":
          verbosio.closeDocument(null);
          break;

        default:
          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
      }
    }
    catch (e) {
      dump(e.stack + "\n\n");
      throw e;
    }

    this.updateCommands();
  },

  // nsIController
  onEvent: function onEvent(aEventName) {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },

  updateCommands: function updateCommands() {
    var commandList = ["cmd_open_files",
                       "cmd_open_pack",
                       "cmd_save",
                       "cmd_close"];
    for (var i = 0; i < commandList.length; i++) {
      var commandName = commandList[i];
      var isEnabled = this.isCommandEnabled(commandName);
      var command = document.getElementById(commandName);
      if (isEnabled) {
        command.removeAttribute("disabled");
      } else {
        command.setAttribute("disabled", true);
      }
    }
  }
};

const baseEditController = {
  // nsIController
  supportsCommand: function supportsCommand(aCommandName) {
    switch (aCommandName) {
      case "cmd_undo":
      case "cmd_redo":
      case "cmd_cut":
      case "cmd_copy":
      case "cmd_paste":
      case "cmd_delete":
      case "cmd_insert":
        return true;
    }

    return false;
  },

  // nsIController
  isCommandEnabled: function isCommandEnabled(aCommandName) {
    var wrapper = documentViewer.currentWrapper;
    if (!wrapper) {
      return false;
    }

    switch (aCommandName) {
      case "cmd_undo":
        return Boolean(gDocumentPack.txmgr.numberOfUndoItems);

      case "cmd_redo":
        return Boolean(gDocumentPack.txmgr.numberOfRedoItems);

      case "cmd_cut":
        return (this.isCommandEnabled("cmd_copy") &&
                this.isCommandEnabled("cmd_delete"));

      case "cmd_copy":
        if (wrapper instanceof C_i.xeIXMLDocumentWrapper) {
          return !(wrapper.selectionRange.collapsed);
        }
        if (wrapper instanceof C_i.xeITextDocumentWrapper) {
          return (wrapper.selectionStartIndex < wrapper.selectionEndIndex);
        }
        return false;

      case "cmd_paste":
        if (wrapper instanceof C_i.xeIXMLDocumentWrapper) {
          return xePasteNodeTransaction.prototype.isCommandEnabled(wrapper);
        }
        if (wrapper instanceof C_i.xeITextDocumentWrapper) {
          return xePasteTextTransaction.prototype.isCommandEnabled(wrapper);
        }

      case "cmd_delete":
        return xeDeleteTransaction.prototype.isCommandEnabled(wrapper);

      case "cmd_insert":
        if (isInstance(wrapper, "xeIXMLDocumentWrapper")) {
          return !(wrapper.selectionRange.collapsed);
        } else if (isInstance(wrapper, "xeITextDocumentWrapper")) {
          return true;
        }
    }

    return false;
  },

  // nsIController
  doCommand: function doCommand(aCommandName) {
    dump("baseEditController.doCommand called with command " + aCommandName + "\n");
    var wrapper = documentViewer.currentWrapper;
    var txn = null;

    try {
      preconditionUIBox.precondition = null;
      switch (aCommandName) {
        case "cmd_undo":
          txn = gDocumentPack.txmgr.peekUndoStack();
          txnDispatcher.undoTransaction(txn);
          // We don't want the transaction to be done.
          return;

        case "cmd_redo":
          txn = gDocumentPack.txmgr.peekRedoStack();
          txnDispatcher.redoTransaction(txn);
          // We don't want the transaction to be done.
          return;

        case "cmd_copy":
        case "cmd_cut":
          verbosio.utils.copySelection(wrapper);
          if (aCommandName == "cmd_copy") {
            // No transaction to do.
            return;
          }
          assert(aCommandName == "cmd_cut", "Going into delete command!");
          // Fall through deliberately, cut relies on delete.
        case "cmd_delete":
          txn = new xeDeleteTransaction(wrapper);
          break;

        case "cmd_insert":
          // Insert commands need a precondition.
          var pre = document.createElementNS(XUL_NS, "precondition");
          pre.setAttribute("type", "insert-content");
          preconditionUIBox.precondition = pre;
          pre.initialize(wrapper);
          return;

        case "cmd_paste":
          if (wrapper instanceof C_i.xeIXMLDocumentWrapper) {
            txn = new xePasteNodeTransaction(wrapper);
          } else if (wrapper instanceof C_i.xeITextDocumentWrapper) {
            txn = new xePasteTextTransaction(wrapper);
          } else {
            throw new Error("Unexpected document wrapper!");
          }
          break;

        default:
          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
      }

      verbosio.advanceTxn(txn, wrapper);
      verbosio.advanceTxn(null, wrapper);
    }
    catch (e) {
      dump(e.stack + "\n\n");
      throw e;
    }

    this.updateCommands();
  },

  // nsIController
  onEvent: function onEvent(aEventName) {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },

  updateCommands: function updateCommands() {
    const commandList = ["cmd_undo",
                         "cmd_redo",
                         "cmd_copy",
                         "cmd_paste",
                         "cmd_delete",
                         "cmd_insert"];
    var copyEnabled = false;
    var deleteEnabled = false;
    for (var i = 0; i < commandList.length; i++) {
      var commandName = commandList[i];
      var isEnabled = this.isCommandEnabled(commandName);
      switch (commandName) {
        case "cmd_copy":
          copyEnabled = isEnabled;
          break;
        case "cmd_delete":
          deleteEnabled = isEnabled;
          break;
      }
      var command = document.getElementById(commandName);
      if (isEnabled) {
        command.removeAttribute("disabled");
      } else {
        command.setAttribute("disabled", true);
      }
    }

    // Cut command relies on copy & delete combined.
    var command = document.getElementById("cmd_cut");
    if (copyEnabled && deleteEnabled) {
      command.removeAttribute("disabled");
    } else {
      command.setAttribute("disabled", true);
    }
  }
};
documentViewer.controllers.appendController(baseEditController);
document.getElementById("editMenu").controllers.appendController(baseEditController);

/**
 * Convenience function to assert the user doesn't do anything before we're ready.
 */
function assertLoadStatus() {
  assert(verbosio.loadStatus == "ready", "Verbosio editor is not yet ready!");
  assert(Zip, "Zip does not exist!");
}

const serializer = new XMLSerializer();
const parser     = new DOMParser();

var dialogStatus = null;

window.addEventListener("DOMWindowClose", function closeWindow(evt) {
  var wrappers = [];
  var currentWrapper = null;
  if (!isInstance(gDocumentPack, "xeIDocumentPack")) {
    var tab = documentViewer.publicURITabs.firstChild;
    while (tab) {
      currentWrapper = tab.getUserData("documentWrapper");
      if (currentWrapper) {
        wrappers[wrappers.length] = currentWrapper;
      }
      tab = tab.nextSibling;
    }
  } else {
    for (var i = 0; i < gDocumentPack.length; i++) {
      wrappers[i] = gDocumentPack.item(i);
    }
  }

  if (gDocumentPack && gDocumentPack.pendingAction) {
    verbosio.advanceTxn(null);
  }

  for (i = 0; i < wrappers.length; i++) {
    verbosio.saveDocument(wrappers[i]);
  }
}, true);

/**
 * When all our overlays are loaded, initialize our editor.
 */
const initializeHandler = {
  /* nsIDOMEventListener */
  handleEvent: function initializeXMLEditor(event) {
    // Anyone along the way may set loadStatus to an error.
    if (verbosio.loadStatus == "uninitialized") {
      verbosio.loadStatus = "ready";
    }

    window.dumpTextbox = document.getElementById("dumpTextbox");
    window.executeText = document.getElementById("executeText");

    // Load overlays as specified by configuration
    verbosio.loadOverlays();

    // Assign ID and ID ref attributes (command, key, etc.)
    verbosio.compileIdsAndRefs();

    documentsMenuPopup.parentNode.addEventListener("command", {
      handleEvent: function handleEvent(evt) {
        var tab = evt.target.getUserData("docTab");
        var index = 0;
        while (tab.previousSibling) {
          index++;
          tab = tab.previousSibling;
        }
        documentViewer.selectDocTab(index);
        documentViewer.publicURITabs.selectedItem = tab;
      }
    }, false);

    viewersMenuPopup.parentNode.addEventListener("command", {
      handleEvent: function handleEvent(evt) {
        var tab = evt.target.getUserData("viewerTab");
        documentViewer.selectViewerTab(tab);
        documentViewer.viewTabs.selectedItem = tab;
      }
    }, false);

    viewersMenuPopup.addEventListener("popupshowing", function showPopup(evt) {
      var item = evt.target.firstChild;
      while (item) {
        var tab = item.getUserData("viewerTab");
        if (tab.hasAttribute("disabled")) {
          item.setAttribute("disabled", "true");
        } else {
          item.removeAttribute("disabled");
        }
        item = item.nextSibling;
      }
    }, true);

    // Arguments handling.
    if (window.arguments.length == 1) {
      var packPath = "";
      if (window.arguments[0] instanceof Components.interfaces.nsICommandLine) {
        if (window.arguments[0].length == 0) {
          return;
        }
        packPath = window.arguments[0].getArgument(0);
      } else {
        packPath = window.arguments[0];
      }
      verbosio.openDocumentPack(packPath);
    }

    window.addEventListener("close", verbosio.closeWindow, true);
  }
};
window.addEventListener("load", initializeHandler, false);
