/* ***** 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) 2006-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 ***** */

const nsITxMgrCID = "@mozilla.org/transactionmanager;1";
var txmgrClass = Components.classes[nsITxMgrCID];
const nsITransactionManager = Components.interfaces.nsITransactionManager;
const nullTxMgr = txmgrClass.createInstance(nsITransactionManager);

function xeTransactionCommon() {
  // xeITransaction
  this.ownerWrapper = null;
}
xeTransactionCommon.prototype = {
  addProperties: function addProperties(aObj) {
    for (var prop in aObj) {
      this[prop] = aObj[prop];
    }
  },

  // xeITransaction
  description: null,

  // nsITransaction
  doTransaction: function doTransaction() {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },

  // nsITransaction
  undoTransaction: function undoTransaction() {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },

  // nsITransaction
  redoTransaction: function redoTransaction() {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },

  // nsITransaction
  isTransient: false,

  // nsITransaction
  merge: function merge(aTransaction) {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },

  // nsIClassInfo
  getInterfaces: function getInterfaces(aCount) {
    var interfaces = [Components.interfaces.xeITransaction,
                      Components.interfaces.nsITransaction,
                      Components.interfaces.nsIClassInfo];
    aCount.value = interfaces.length;
    return interfaces;
  },

  // nsIClassInfo
  getHelperForLanguage: function getHelperForLanguage(aLanguage) {
    return null;
  },

  // nsIClassInfo
  classDescription: null,

  // nsIClassInfo
  classID: null,

  // nsIClassInfo
  contractID: null,

  // nsIClassInfo
  implementationLanguage:
  Components.interfaces.nsIProgrammingLanguage.JAVASCRIPT,

  // nsIClassInfo
  flags: 0,

  // nsISupports
  QueryInterface: function QueryInterface(aIID)
  {
    if (aIID.equals(Components.interfaces.nsISupports))
      return this;

    var i;
    var interfaces = this.getInterfaces({});
    for (i = 0; i < interfaces.length; i++) {
      if (aIID.equals(interfaces[i])) {
        return this;
      }
    }

    // Discover what interfaces we need
    i = Components.interfaces;
    var found = false;
    for (var prop in i) {
      if (aIID.equals(i[prop])) {
        found = true;
        dump("QUERYINTERFACE xeTransactionCommon: " + prop + "\n");
        break;
      }
    }
    if (!found) {
      dump("QUERYINTERFACE xeTransactionCommon: Unknown\n");
    }

    throw Components.results.NS_ERROR_NO_INTERFACE;
    return null;
  }
}

function NoOpTxn() {
  this.wrappedJSObject = this;
}
NoOpTxn.prototype = new xeTransactionCommon();
NoOpTxn.prototype.addProperties({
  doTransaction: function doTransaction(aNode) {
    // Do nothing.
  },

  undoTransaction: function undoTransaction(aNode) {
    // Do nothing.
  },

  redoTransaction: function redoTransaction(aNode) {
    // Do nothing.
  },

  merge: function merge(aTransaction) {
    return false;
  }
});

/**
 * Build a nsITransaction object for a text node, based on a textbox.
 *
 * @param aNode The text node to modify.
 * @param aOldValue The original value of the node (before the transaction).
 * @param aNewValue A user-input value for the node (after the transaction).
 * @param aTextbox The XUL textbox used to edit the text node.
 *
 * @return nsITransaction object from constructor.
 */
function UpdateTextNodeTxn(aNode, aOldValue, aNewValue, aTextbox) {
  this.node     = aNode;
  this.oldValue = aOldValue;
  this.newValue = aNewValue;
  this.textbox  = aTextbox;
  this.textTxMgr = aTextbox.editor.transactionManager;
  this.isDone = false;
  this.wrappedJSObject = this;
}
UpdateTextNodeTxn.prototype = new xeTransactionCommon();
UpdateTextNodeTxn.prototype.addProperties({
  name: "UpdateTextNodeTxn",

  /**
   * Reset the textbox associated with this node.
   *
   * @param aValue The textbox value (based on the node value) to set.
   */
  resetTextbox: function resetTextbox(aValue) {
    var targetEditNode = this.textbox.getUserData("targetEditNode");
    if (targetEditNode != this.node) {
      // Don't change the textbox; it's pointing to something else.
      return;
    }
    var origTxMgr = this.textbox.editor.transactionManager;
    this.textbox.editor.transactionManager = nullTxMgr;
    this.textbox.value = aValue;
    this.textbox.editor.transactionManager = origTxMgr;
    nullTxMgr.clear();
  },

  // nsITransaction
  doTransaction: function doTransaction() {
    this.node.nodeValue = this.newValue;
    // The user can't be allowed to undo textbox changes after this txn commits.
    this.textTxMgr.clear();
    this.textTxMgr = null;
    this.isDone = true;
    dump("UpdateTextNodeTxn done!  New value:\n" + this.newValue + "\n(end new value)\n");
  },

  // nsITransaction
  undoTransaction: function undoTransaction() {
    this.node.nodeValue = this.oldValue;
    this.resetTextbox(this.oldValue);
  },

  // nsITransaction
  redoTransaction: function redoTransaction() {
    this.resetTextbox(this.newValue);
    this.node.nodeValue = this.newValue;
  },

  // nsITransaction
  isTransient: false,

  // nsITransaction
  merge: function merge(aTxn) {
    rv = false;
    if (this.isDone) {
      return rv;
    }

    var txn = (typeof aTxn.wrappedJSObject == "object") ?
              aTxn.wrappedJSObject :
              null;
    if (txn && txn instanceof UpdateTextNodeTxn && txn.node == this.node) {
      this.newValue = txn.newValue;
      rv = true;
    }

    return rv;
  },

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

/**
 * Manage a single DOM data node with a text box.
 *
 * @param aTextbox XUL textbox for editing the node.
 * @param aTargetNode The DOM node being edited.
 * @param aDocumentWrapper Wrapper for the document owning the target node.
 *
 * @return nsITransactionListener object from constructor.
 */
function TextboxTxnListener(aTextbox, aTargetNode, aDocumentWrapper) {
  this.targetNode = aTargetNode;
  this.textbox = aTextbox;
  this.wrapper = aDocumentWrapper;

  aTextbox.setUserData("targetEditNode", aTargetNode, null);
}
TextboxTxnListener.prototype = {
  /**
   * Get the wrapped JS object for a transaction.
   *
   * @param aTransaction nsITransaction object.
   *
   * @return JS object
   */
  getWrappedTxn: function getWrappedTxn(aTransaction) {
    var txn = (aTransaction && (typeof aTransaction.wrappedJSObject == "object")) ?
              aTransaction.wrappedJSObject :
              null;
    return txn;
  },

  // nsITransactionListener
  willDo: function willDo(aManager, aTransaction) {
    this.originalText = this.textbox.value;
  },

  // nsITransactionListener
  didDo: function didDo(aManager, aTransaction, aResult) {
    if (aResult > 0) {
      // Stupid transaction error.
      baseEditController.updateCommands();
      return;
    }

    // Create the transaction.
    var txn = new UpdateTextNodeTxn(this.targetNode,
                                    this.originalText,
                                    this.textbox.value,
                                    this.textbox);

    // Push the new transaction onto the stack.
    verbosio.advanceTxn(txn, this.wrapper);
  },

  // nsITransactionListener
  willUndo: function willUndo(aManager, aTransaction) {
  },

  // nsITransactionListener
  didUndo: function didUndo(aManager, aTransaction, aResult) {
  },

  // nsITransactionListener
  willRedo: function willRedo(aManager, aTransaction) {
  },

  // nsITransactionListener
  didRedo: function didRedo(aManager, aTransaction, aResult) {
  },

  // nsITransactionListener
  willBeginBatch: function willBeginBatch(aManager) {
  },

  // nsITransactionListener
  didBeginBatch: function didBeginBatch(aManager, aResult) {
  },

  // nsITransactionListener
  willEndBatch: function willEndBatch(aManager) {
  },

  // nsITransactionListener
  didEndBatch: function didEndBatch(aManager, aResult) {
  },

  // nsITransactionListener
  willMerge: function willMerge(aManager, aTopTxn, aTxnToMerge) {
  },

  // nsITransactionListener
  didMerge: function didMerge(aManager, aTopTxn, aTxnToMerge, aDidMerge, aMergeResult) {
  },

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

/**
 * Construct an attribute change action object for a recorded transaction.
 *
 * @param attrNode    The attribute node being changed.
 * @param attrChange  The attrChange property of the base event.
 * @param elementNode The element owning the attribute.
 * @param prevValue   The previous value of the attribute.
 * @param newValue    The new value of the attribute.
 */
function AttrChangeAction(attrNode, attrChange, elementNode, prevValue, newValue) {
  this.attrNode = attrNode;
  this.attrChange = attrChange;
  this.elementNode = elementNode;
  this.prevValue = prevValue;
  this.newValue = newValue;
}
AttrChangeAction.prototype = {
  /**
   * Undo the specified action.
   */
  undoAction: function undoAction() {
    const nsIDOMMutationEvent = Components.interfaces.nsIDOMMutationEvent;
    switch (this.attrChange) {
      case nsIDOMMutationEvent.MODIFICATION:
        this.attrNode.nodeValue = this.prevValue;
        break;

      case nsIDOMMutationEvent.ADDITION:
        this.elementNode.removeAttributeNode(this.attrNode);
        break;

      case nsIDOMMutationEvent.REMOVAL:
        this.elementNode.setAttributeNodeNS(this.attrNode);
        break;
    }
  },

  /**
   * Redo the specified action.
   */
  redoAction: function redoAction() {
    const nsIDOMMutationEvent = Components.interfaces.nsIDOMMutationEvent;
    switch (this.attrChange) {
      case nsIDOMMutationEvent.MODIFICATION:
        this.attrNode.nodeValue = this.newValue;
        break;

      case nsIDOMMutationEvent.ADDITION:
        this.elementNode.setAttributeNodeNS(this.attrNode);
        break;

      case nsIDOMMutationEvent.REMOVAL:
        this.elementNode.removeAttributeNode(this.attrNode);
        break;
    }
  }
}

/**
 * Construct a character data change action object for a recorded transaction.
 *
 * @param dataNode  The data node being changed.
 * @param prevValue The previous value of the data node.
 * @param newValue  The new value of the data node.
 */
function CDATAModifiedAction(dataNode, prevValue, newValue) {
  this.dataNode = dataNode;
  this.prevValue = prevValue;
  this.newValue = newValue;
}
CDATAModifiedAction.prototype = {
  /**
   * Undo the specified action.
   */
  undoAction: function undoAction() {
    this.dataNode.nodeValue = prevValue;
  },

  /**
   * Redo the specified action.
   */
  redoAction: function redoAction() {
    this.dataNode.nodeValue = newValue;
  }
}

/**
 * Construct a node insertion action object for a recorded transaction.
 *
 * @param parentNode The parent of the inserted node.
 * @param childNode  The inserted node.
 */
function NodeInsertedAction(parentNode, childNode) {
  this.parentNode = parentNode;
  this.childNode = childNode;
  this.nextSibling = childNode.nextSibling;
}
NodeInsertedAction.prototype = {
  /**
   * Undo the specified action.
   */
  undoAction: function undoAction() {
    this.parentNode.removeChild(this.childNode);
  },

  /**
   * Redo the specified action.
   */
  redoAction: function redoAction() {
    this.parentNode.insertBefore(this.childNode, this.nextSibling);
  }
}

/**
 * Construct a node removal action object for a recorded transaction.
 *
 * @param parentNode The parent of the removed node.
 * @param childNode  The removed node.
 */
function NodeRemovedAction(parentNode, childNode) {
  this.parentNode = parentNode;
  this.childNode = childNode;
  this.nextSibling = childNode.nextSibling;
}
NodeRemovedAction.prototype = {
  /**
   * Undo the specified action.
   */
  undoAction: function undoAction() {
    this.parentNode.insertBefore(this.childNode, this.nextSibling);
  },

  /**
   * Redo the specified action.
   */
  redoAction: function redoAction() {
    this.parentNode.removeChild(this.childNode);
  }
}

/**
 * Construct an action for undoing and redoing a DTD entity change.
 *
 * @param aEntity The entity to update.
 */
function EntityChangeAction(aEntity) {
  assert(aEntity instanceof Components.interfaces.xeIEntity,
         "Not a xeIEntity object?");
  this._entity = aEntity;
  this._newValue = aEntity.source;
  this._oldValue = aEntity.ownerDTD.getEntityValue(aEntity.name);
}
EntityChangeAction.prototype = {
  /**
   * Do the specified action.
   */
  doAction: function doAction() {
    this._entity.persistValue();
  },

  undoAction: function undoAction() {
    this._entity.acceptNewValue(this._oldValue);
    this.doAction();
  },

  redoAction: function redoAction() {
    this._entity.acceptNewValue(this._newValue);
    this.doAction();
  }
}

/**
 * A generic event recorder for recorded transactions.
 */
const EventRecorder = {
  // The currently active transaction.
  targetTxn: null,

  /**
   * Start recording the DOM changes for a transaction.
   *
   * @param aTxn The transaction to record changes for.
   */
  startRecording: function startRecording(aTxn) {
    assert(!this.targetTxn, "Already recording?");
    assert(aTxn instanceof xeRecordedTransaction, "Not a recordable transaction?");
    this.targetTxn = aTxn;
  },

  /**
   * Stop recording the DOM changes for a transaction.
   *
   * @param aTxn The transaction to record changes for.
   */
  stopRecording: function stopRecording(aTxn) {
    assert(this.targetTxn, "Not recording anything?");
    assert(this.targetTxn == aTxn, "Transaction didn't match?");
    this.targetTxn = null;
  },

  /**
   * Record an attribute change action.
   *
   * @param aEvent DOM Event object to base the action on.
   */
  attrChangeEventHandler: function handleAttrChangeEvent(aEvent) {
    var action = new AttrChangeAction(aEvent.relatedNode,
                                      aEvent.attrChange,
                                      aEvent.target,
                                      aEvent.prevValue,
                                      aEvent.newValue);
    this.targetTxn.recordedActions.push(action);
  },

  /**
   * Record a data node change action.
   *
   * @param aEvent DOM Event object to base the action on.
   */
  cdataModifiedEventHandler: function handleCDATAModEvent(aEvent) {
    var action = new CDATAModifiedAction(aEvent.target,
                                         aEvent.prevValue,
                                         aEvent.newValue);
    this.targetTxn.recordedActions.push(action);
  },

  /**
   * Record a node insertion action.
   *
   * @param aEvent DOM Event object to base the action on.
   */
  nodeInsertedEventHandler: function nodeInsertedHandler(aEvent) {
    var action = new NodeInsertedAction(aEvent.relatedNode,
                                        aEvent.target);
    this.targetTxn.recordedActions.push(action);
  },

  /**
   * Record a node removal action.
   *
   * @param aEvent DOM Event object to base the action on.
   */
  nodeRemovedEventHandler: function nodeInsertedHandler(aEvent) {
    var action = new NodeRemovedAction(aEvent.relatedNode,
                                       aEvent.target);
    this.targetTxn.recordedActions.push(action);
  },

  /**
   * Add the event recorder's event listeners to a XML document.
   *
   * @param aWrapper Wrapper for the XML document.
   */
  addToWrapper: function addToWrapper(aWrapper) {
    const xeIXMLDocumentWrapper = Components.interfaces.xeIXMLDocumentWrapper;
    assert(aWrapper instanceof xeIXMLDocumentWrapper, "Not a XML document?");

    var doc = aWrapper.wrappedDocument;
    doc.addEventListener("DOMAttrModified", this, true);
    doc.addEventListener("DOMCharacterDataModified", this, true);
    doc.addEventListener("DOMNodeInserted", this, true);
    doc.addEventListener("DOMNodeRemoved", this, true);
  },

  /**
   * Remove the event recorder's event listeners from a XML document.
   *
   * @param aWrapper Wrapper for the XML document.
   */
  removeFromWrapper: function removeFromWrapper(aWrapper) {
    const xeIXMLDocumentWrapper = Components.interfaces.xeIXMLDocumentWrapper;
    assert(aWrapper instanceof xeIXMLDocumentWrapper, "Not a XML document?");

    var doc = aWrapper.wrappedDocument;
    doc.removeEventListener("DOMAttrModified", this, true);
    doc.removeEventListener("DOMCharacterDataModified", this, true);
    doc.removeEventListener("DOMNodeInserted", this, true);
    doc.removeEventListener("DOMNodeRemoved", this, true);
  },

  // nsIDOMEventListener
  handleEvent: function handleEvent(aEvent) {
    if (!this.targetTxn || (aEvent.target != aEvent.originalTarget)) {
      return;
    }

    switch (aEvent.type) {
      case "DOMAttrModified":
        this.attrChangeEventHandler(aEvent);
        break;

      case "DOMCharacterDataModified":
        this.cdataModifiedEventHandler(aEvent);
        break;

      case "DOMNodeInserted":
        this.nodeInsertedEventHandler(aEvent);
        break;

      case "DOMNodeRemoved":
        this.nodeRemovedEventHandler(aEvent);
        break;

      default:
        throw new Error("Unexpected event type: " + aEvent.type);
    }
  }
}

/**
 * Construct a transaction which records the DOM mutations that occur with it.
 */
function xeRecordedTransaction() {
  this.recordedActions = [];
  this.transactionDone = false;
  this.wrappedJSObject = this;
}
xeRecordedTransaction.prototype = new xeTransactionCommon();
xeRecordedTransaction.prototype.addProperties({
  // xeIRecordedTransaction
  doRecordedTransaction: function doRecordedTransaction() {
    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  },

  // nsITransaction
  doTransaction: function doTransaction() {
    EventRecorder.startRecording(this);

    try {
      this.doRecordedTransaction();
    }
    catch (e) {
      EventRecorder.stopRecording(this);
      throw e;
    }

    EventRecorder.stopRecording(this);
    this.transactionDone = true;
  },

  // nsITransaction
  undoTransaction: function undoTransaction() {
    if (!this.transactionDone) {
      throw Components.results.NS_ERROR_FAILURE;
    }
    for (var i = this.recordedActions.length - 1; i >= 0; i--) {
      var action = this.recordedActions[i];
      action.undoAction();
    }
  },

  // nsITransaction
  redoTransaction: function redoTransaction() {
    if (!this.transactionDone) {
      throw Components.results.NS_ERROR_FAILURE;
    }
    for (var i = 0; i < this.recordedActions.length; i++) {
      var action = this.recordedActions[i];
      action.redoAction();
    }
  },

  // nsITransaction
  isTransient: false,

  // nsITransaction
  merge: function merge(aTxn) {
    return false;
  },

  // nsIClassInfo
  getInterfaces: function getInterfaces(aCount) {
    var interfaces = [Components.interfaces.xeIRecordedTransaction,
                      Components.interfaces.xeITransaction,
                      Components.interfaces.nsITransaction,
                      Components.interfaces.nsIClassInfo];
    aCount.value = interfaces.length;
    return interfaces;
  }
});

/**
 * Construct a transaction to delete the selection from a document.
 *
 * @param aWrapper Wrapper for the document.
 */
function xeDeleteTransaction(aWrapper) {
  this.wrappedJSObject = this;
  this.wrapper = aWrapper;
  this.isXMLTransaction = false;
  if (isInstance(this.wrapper, "xeIXMLDocumentWrapper")) {
    this.isXMLTransaction = true;
    this.range = aWrapper.selectionRange.cloneRange();
  }
}
xeDeleteTransaction.prototype = new xeRecordedTransaction();
xeDeleteTransaction.prototype.addProperties({
  // xeIRecordedTransaction
  doRecordedTransaction: function doRecordedTransaction() {
    if (this.isXMLTransaction) {
      this.range.deleteContents();
    }
  },

  // xeITransaction
  // XXX ajvincent Localize!
  description: "Delete Selection",

  /**
   * Report if a delete command can be done.
   *
   * @param aWrapper xeIDocumentWrapper object requesting deletion.
   *
   * @return true if the command is enabled.
   */
  isCommandEnabled: function isCommandEnabled(aWrapper) {
    if (isInstance(aWrapper, "xeIXMLDocumentWrapper")) {
      var baseRange = aWrapper.selectionRange;

      if (baseRange.collapsed) {
        return false;
      }

      var doc = aWrapper.wrappedDocument;

      // Make sure the document element isn't in the range.
      var docElementIndex = 0;
      var iterator = doc.documentElement.previousSibling;
      while (iterator) {
        docElementIndex++;
        iterator = iterator.previousSibling;
      }
      var isRootContained = baseRange.isPointInRange(doc, docElementIndex);
      if (isRootContained) {
        return false;
      }

      return true;
    }

    return false;
  }
});

/**
 * Delete an attribute.
 *
 * @param aAttrNode The attribute node to delete.
 */
function DeleteAttrTxn(aAttrNode) {
  this.attrNode = aAttrNode;
  this.ownerElement = aAttrNode.ownerElement;
  this.value = aAttrNode.nodeValue;
  this.wrappedJSObject = this;
}
DeleteAttrTxn.prototype = new xeRecordedTransaction();
DeleteAttrTxn.prototype.addProperties({
  name: "DeleteAttrTxn",

  // xeIRecordedTransaction
  doRecordedTransaction: function doRecordedTransaction() {
    this.ownerElement.removeAttributeNode(this.attrNode);
  },

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

/**
 * Construct a transaction to split a text node.
 *
 * @param aNode   Text node to split.
 * @param aOffset Index at which to split the text node.
 */
function xeSplitTextTransaction(aNode, aOffset) {
  assert(typeof aOffset == "number", "Not a non-negative integer?");
  assert(aOffset >= 0, "Not a non-negative integer?");
  assert(Math.floor(aOffset) == aOffset, "Not a non-negative integer?");
  assert(aOffset < Number.POSITIVE_INFINITY, "Not a non-negative integer?");

  const nsIDOMText = Components.interfaces.nsIDOMText;
  assert(this.firstNode instanceof nsIDOMText, "Not a text node?");
  this.firstNode = aNode;
  this.offset = aOffset;
}
xeSplitTextTransaction.prototype = new xeRecordedTransaction();
xeSplitTextTransaction.prototype.addProperties({
  // xeITransaction
  // XXX ajvincent Localize!
  description: "Split text",

  // xeIRecordedTransaction
  doRecordedTransaction: function doRecordedTransaction() {
    this.firstNode.splitText(this.offset);
  }
});

/**
 * Construct a transaction to past the selection into a XML document.
 *
 * @param aWrapper Wrapper for the document.
 */
function xePasteNodeTransaction(aWrapper) {
  assert(aWrapper instanceof C_i.xeIXMLDocumentWrapper, "Not a XML wrapper?");
  this.wrappedJSObject = this;
  this.wrapper = aWrapper;
  this.range = aWrapper.selectionRange.cloneRange();
}
xePasteNodeTransaction.prototype = new xeRecordedTransaction();
xePasteNodeTransaction.prototype.addProperties({
  // xeIRecordedTransaction
  doRecordedTransaction: function doRecordedTransaction() {
    var clipText;
    const CLIP_NODE = "application/x-vnd.verbosio.dom-node";
    var doc = this.wrapper.wrappedDocument;
    var clipType = verbosio.utils.clipboardType;
    var clipNode = null;
    if (!clipType || clipType != CLIP_NODE) {
      clipText = verbosio.utils.getClipboardContents("text/plain", {});
      clipNode = doc.createCDATASection(clipText);
    } else {
      clipNode = {};
      var clipText = verbosio.utils.getClipboardContents(CLIP_NODE, clipNode);
      clipNode = clipNode.value;
      assert(clipNode instanceof C_i.nsIDOMNode, "No node returned?");
      doc.adoptNode(clipNode);
    }

    this.range.deleteContents();
    this.range.insertNode(clipNode);
  },

  // xeITransaction
  // XXX ajvincent Localize!
  description: "Paste node over selection",

  /**
   * Report if a paste command can be done.
   *
   * @param aWrapper xeIDocumentWrapper object requesting deletion.
   *
   * @return true if the command is enabled.
   */
  isCommandEnabled: function isCommandEnabled(aWrapper) {
    assert(aWrapper instanceof C_i.xeIXMLDocumentWrapper, "Bad wrapper!");

    const CLIP_NODE = "application/x-vnd.verbosio.dom-node";
    var clipType = verbosio.utils.clipboardType;
    var clipNode = null;
    if (clipType && clipType == CLIP_NODE) {
      clipNode = {};
      var clipText = verbosio.utils.getClipboardContents(CLIP_NODE, clipNode);
      clipNode = clipNode.value;
      assert(clipNode instanceof C_i.nsIDOMNode, "No node returned?");
      if (!clipNode) {
        return false;
      }
    }

    var doc = aWrapper.wrappedDocument;
    var baseRange = aWrapper.selectionRange;

    // Make sure the document element isn't in the range.
    var docElementIndex = 0;
    var iterator = doc.documentElement.previousSibling;
    while (iterator) {
      docElementIndex++;
      iterator = iterator.previousSibling;
    }
    var isRootContained = baseRange.isPointInRange(doc, docElementIndex);
    return !isRootContained;
  }
});

/**
 * Construct a transaction to past the selection into a text document.
 *
 * @param aWrapper Wrapper for the document.
 */
function xePasteTextTransaction(aWrapper) {
  assert(aWrapper instanceof C_i.xeITextDocumentWrapper, "Bad aWrapper!");
  this.wrapper = aWrapper;
  clipText = verbosio.utils.getClipboardContents("text/plain", {});
  assert(clipText, "Is this command enabled?");
  this.originalSource = this.wrapper.wrappedSource;
  var part1 = source.substr(0, this.wrapper.selectionStartIndex);
  var part3 = source.substr(this.wrapper.selectionEndIndex);
  this.newSource = part1 + clipText + part3;
}
xePasteTextTransaction.prototype = new xeTransactionCommon();
xePasteTextTransaction.prototype.addProperties({
  // XXX ajvincent Localize!
  description: "Paste text over selection",

  doTransaction: function doTransaction() {
    this.wrapper.reparseSource(this.newSource);
  },

  undoTransaction: function undoTransaction() {
    this.wrapper.reparseSource(this.originalSource);
  },

  redoTransaction: function redoTransaction() {
    this.wrapper.reparseSource(this.newSource);
  },

  merge: function merge(aTxn) {
    return false;
  },

  isCommandEnabled: function isCommandEnabled(aWrapper) {
    assert(aWrapper instanceof C_i.xeITextDocumentWrapper, "Bad aWrapper!");
    var clipText = verbosio.utils.getClipboardContents("text/plain", {});
    return Boolean(clipText);
  }
});
