/* ***** 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 Entity Manager.
 *
 * The Initial Developer of the Original Code is
 * Alexander J. Vincent <ajvincent@gmail.com>.
 * Portions created by the Initial Developer are Copyright (C) 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 ***** */

// Get JS modules.
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://app/modules/ArrayConverter.jsm");
Components.utils.import("resource://app/modules/ecma-debug.jsm");
Components.utils.import("resource://app/modules/XMLValidator.jsm");

// Utility function.
function ERROR_FAILURE() {
  throw Components.results.NS_ERROR_FAILURE;
}

// Utility function.
function NOT_IMPLEMENTED() {
  throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
}

// Utility function.
function ERROR_ABORT() {
  dumpJSStack();
  throw Components.results.NS_ERROR_ABORT;
}

// Utility function
function NOT_INITIALIZED() {
  throw Components.results.NS_ERROR_NOT_INITIALIZED;
}

// Utility function
function ALREADY_INITIALIZED() {
  throw Components.results.NS_ERROR_ALREADY_INITIALIZED;
}

const C_i = Components.interfaces;

const xeIEntity                   = C_i.xeIEntity;
const xeIEntityManager            = C_i.xeIEntityManager;
const xeIXMLDocumentWrapper       = C_i.xeIXMLDocumentWrapper;
const xeIDocumentWrapper          = C_i.xeIDocumentWrapper;
const xeIDTDWrapper               = C_i.xeIDTDWrapper;
const xeIVerbosioUtilsService     = C_i.xeIVerbosioUtilsService;
const nsIDOMParser                = C_i.nsIDOMParser;
const nsIDOMNode                  = C_i.nsIDOMNode;
const nsIDOM3Node                 = C_i.nsIDOM3Node;
const nsIDOMAttr                  = C_i.nsIDOMAttr;
const nsIDOMCharacterData         = C_i.nsIDOMCharacterData;
const nsIDOMDocumentFragment      = C_i.nsIDOMDocumentFragment;
const nsIDOMDocumentRange         = C_i.nsIDOMDocumentRange;
const nsIDOMNodeFilter            = C_i.nsIDOMNodeFilter;
const nsIDOMDocumentXBL           = C_i.nsIDOMDocumentXBL;
const mozIJSSubScriptLoader       = C_i.mozIJSSubScriptLoader;
const nsICategoryManager          = C_i.nsICategoryManager;
const nsIScriptError              = C_i.nsIScriptError;
const nsIProgrammingLanguage      = C_i.nsIProgrammingLanguage;
const nsISecurityCheckedComponent = C_i.nsISecurityCheckedComponent;
const nsIClassInfo                = C_i.nsIClassInfo;
const nsISupports                 = C_i.nsISupports;

var vbUtils = null; // set in EntityManager() constructor

const parser     = Components.classes["@mozilla.org/xmlextras/domparser;1"]
                             .createInstance(nsIDOMParser);

const ENTITY_STATUS = {
  NOT_INITIALIZED: 0,
  NO_MATCH: 1,
  PARTIAL_MATCH: 2,
  COMPLETE_MATCH: 3
};

const DATA_MATCH = {
  START: 100,
  FULL: 101,
  END: 102
};

const MARKUP_NS = "http://verbosio.mozdev.org/namespaces/markup/";

/**
 * Construct a new xeIEntity object.
 *
 * @param aOwnerDTD    The DTDWrapper owning this entity.
 * @param aName        The name of the entity.
 * @param aSource      The source code of the entity.
 * @param aDocType     The doctype source (used for parsing the entity into a
 *                     well-formed DOM document fragment)
 * @param aEntityMgr   The entity manager which owns this.
 * @param aFragment    Generated fragment to clone (null for original entity).
 */
function Entity(aOwnerDTD,
                aName,
                aSource,
                aDocType,
                aEntityMgr,
                aFragment) {
  if (!(aOwnerDTD instanceof xeIDTDWrapper) ||
      !(aName) ||
      !(aSource) ||
      !(aDocType) ||
      !(aEntityMgr instanceof EntityManager) ||
      (aFragment && !(aFragment instanceof nsIDOMDocumentFragment))) {
    dumpJSStack();
    throw Components.results.NS_ERROR_INVALID_ARG;
  }

  // xeIEntity
  this.ownerDTD = aOwnerDTD;
  this.name = aName;
  this.source = aSource;

  // Internal values
  this._storedSource = aSource;
  this._entityMgr = aEntityMgr;

  if (aDocType.indexOf("<!DOCTYPE") == -1) {
    aDocType = "<!DOCTYPE wrapper [" + aDocType + "]>";
  }
  this._doctype = aDocType;
  if (aFragment) {
    this._fragment = aFragment.cloneNode(true);
  } else {
    this._fragment = null;
    this._fragment = this.getEntityFragment(aSource);
  }

  /* XXX ajvincent We should not need to keep a reference to the owner document,
     but the native DOM as of Gecko 1.9a5pre doesn't do it for us.  Having it
     here is just asking for trouble.
  */
  assert(this._fragment, "No fragment?");
  assert(this._fragment.ownerDocument, "No owner doc?");
  this._ownerDoc = this._fragment.ownerDocument;

  this._status = ENTITY_STATUS.NOT_INITIALIZED;

  // xeIEntity
  this.safeForAttributes = this.checkFragmentSafety(this._fragment);

  this._cloneList = [this];
}
Entity.prototype = {
  /**
   * Convert an entity declaration into a DOM fragment, if possible.
   *
   * @param str Source code of the entity.
   *
   * @return nsIDOMDocumentFragment the entity can parse into, or null.
   */
  getEntityFragment: function getEntityFragment(str) {
    var startTag = "<wrapper ";
    const endTag = "</wrapper>";
    var namespaces = "";
    const namespaceMap = {
      xmlns: "http://www.w3.org/2000/xmlns/",
      xml:   "http://www.w3.org/XML/1998/namespace"
    };
    var namespaceCount = 0;

    const prefixMsg = "XML Parsing Error: prefix not bound to a namespace";
    var source;
    do {
      // First, run the source code through the SAX reader.
      source = this._doctype + startTag + namespaces + ">" + str + endTag;

      var caughtErr = false;
      try {
        XMLValidator.validate(source);
        // No errors!  We can leave now.
        break;
      } catch (e if (e == Components.results.NS_ERROR_ABORT)) {
        // What type of well-formedness error was it?
        var msg = XMLValidator.error;
        msg = msg.substr(0, msg.indexOf("\n"));
        if (msg != prefixMsg) {
          // Busted!  We can't recover from this.
          warn(XMLValidator.error);
          dump(source + "\n\n");
          return null;
        }
      }

      // We can recover from this, by declaring namespaces on the wrapper node.
      // Get the tag where the well-formedness error appeared.
      var lines = source.split("\n");
      lines.splice(0, XMLValidator.locator.lineNumber - 1);
      source = lines.join("\n");
      var columnNumber = XMLValidator.locator.columnNumber;
      var tagSource = source.substring(columnNumber - 1,
                                       source.indexOf(">", columnNumber) + 1);

      // Set the needed namespaces.

      const nsElemRE = /\<([^\:]*):\w*\s*/m;
      var elemMatch = nsElemRE.exec(tagSource);
      if (elemMatch && !(elemMatch[1] in namespaceMap)) {
        namespaceMap[elemMatch[1]] = "verbosio:" + namespaceCount;
        namespaceCount++;
        namespaces += "xmlns:" + elemMatch[1] + "='verbosio:"
                   + namespaceCount + "' ";
      }

      const nsAttrRE = /([^\:]*):\w*\s*=\s*([\'\"])/m;
      var attrMatch = nsAttrRE.exec(tagSource);

      try {
        assert(elemMatch || attrMatch, "No namespace match???");
      } catch (e) {
        dump(tagSource + "\n\n");
        dump(startTag + namespaces + ">" + str + endTag + "\n\n");
        throw e;
      }

      while (attrMatch) {
        if (!(attrMatch[1] in namespaceMap)) {
          namespaceMap[attrMatch[1]] = "verbosio:" + namespaceCount;
          namespaceCount++;
          namespaces += "xmlns:" + attrMatch[1] + "='verbosio:"
                     + namespaceCount + "' ";
        }
        tagSource = tagSource.substr(tagSource.indexOf(attrMatch[0]) +
                                     attrMatch[0].length);
        tagSource = tagSource.substr(tagSource.indexOf(attrMatch[2]) + 1)
        attrMatch = nsAttrRE.exec(tagSource);
      }
    } while (true);

    assert(!(XMLValidator.error), "We shouldn't have any errors here!");

    var wrapperDoc = parser.parseFromString(source, "application/xml");
    wrapperDoc.normalize();
    assert(wrapperDoc instanceof nsIDOMDocumentRange, "Didn't get document!");

    var range = wrapperDoc.createRange();
    range.setStartBefore(wrapperDoc.documentElement.firstChild);
    range.setEndAfter(wrapperDoc.documentElement.lastChild);
    return range.extractContents();
  },

  /**
   * Check to see if a document fragment from an entity is safe for attributes.
   *
   * @param aFragment The fragment to check.
   */
  checkFragmentSafety: function checkFragmentSafety(aFragment) {
    assert(aFragment instanceof nsIDOMDocumentFragment, "Where's the beef?");
    var node = aFragment.firstChild;
    assert(node, "No fragment first child??");
    while (node) {
      if (!(node instanceof nsIDOMCharacterData)) {
        return false;
      }
      node = node.nextSibling;
    }
    return true;
  },

  // xeIEntity
  acceptNewValue: function acceptNewValue(aValue) {
    var fragment = this.getEntityFragment(aValue);
    if (!fragment) {
      throw Components.results.NS_ERROR_INVALID_ARG;
    }
    if (this.safeForAttributes && !this.checkFragmentSafety(fragment)) {
      throw Components.results.NS_ERROR_INVALID_ARG;
    }
    this._fragment = fragment;
    this.source = aValue;
  },

  // xeIEntity
  persistValue: function persistValue() {
    dump("old value: " + this._storedSource + "\n");
    dump("new value: " + this.source + "\n\n");
    this._entityMgr.persistValue(this);
  },

  // xeIEntity
  clone: function clone() {
    var ent = new Entity(this.ownerDTD,
                         this.name,
                         this.source,
                         this._doctype,
                         this._entityMgr,
                         this._fragment);
    ent.safeForAttributes = this.safeForAttributes;
    ent._ownerNode = this._ownerNode;
    ent._ownerDoc = this._ownerDoc;
    ent._storedSource = this._storedSource;
    ent._cloneList = this._cloneList;
    ent._cloneList[ent._cloneList.length] = ent;
    return ent;
  },

  // nsISupports
  QueryInterface: function QueryInterface(aIID) {
    if (aIID.equals(xeIEntity) ||
        aIID.equals(nsISupports))
      return this;

    // Known interfaces we don't support.
    if (aIID.equals(nsIClassInfo) ||
        aIID.equals(nsISecurityCheckedComponent)) {
      throw Components.results.NS_ERROR_NO_INTERFACE;
      return null;
    }

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

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

  toString: function toString() {
    return "[object Entity (" + this.name + " = '" + this.source +
           "' in " + this.ownerDTD.baseWrapper.publicURI + ")]";
  }
}

function EntityManager()
{
  if (!vbUtils) {
    vbUtils = Components.classes["@verbosio.mozdev.org/verbosio-utils;1"]
                        .getService(xeIVerbosioUtilsService);
  }
  this._mappedDTDs = {};
  this._docWrapper = null;
  this._docPack = null;
  this._entitiesReplaced = 0;
}
EntityManager.prototype =
{
  checkInitialized: function checkInitialized() {
    if (!this._docPack) {
      throw Components.results.NS_ERROR_NOT_INITIALIZED;
    }
  },

  checkNotInitialized: function checkNotInitialized() {
    if (this._docPack) {
      throw Components.results.NS_ERROR_ALREADY_INITIALIZED;
    }
  },

  /**
   * Build and sort entities in a DTD wrapper.
   *
   * @param aDTDWrapper to get entities from.
   */
  mapDTD: function mapDTD(aDTDWrapper) {
    assert(aDTDWrapper instanceof xeIDTDWrapper, "Not a DTD wrapper?");
    assert(aDTDWrapper.baseWrapper instanceof xeIDocumentWrapper,
           "Not a document wrapper???");
    var uri = aDTDWrapper.baseWrapper.publicURI;
    assert(uri, "No publicURI for a document wrapper?");
    if (uri in this._mappedDTDs) {
      return;
    }

    assert(aDTDWrapper instanceof xeIDTDWrapper, "Not a DTD wrapper?");
    var aCount = {};
    var i;
    var subWrappers = aDTDWrapper.getExternalDTDWrappers(aCount);
    for (i = 0; i < aCount.value; i++) {
      this.mapDTD(subWrappers[i]);
    }

    var doctype = (aDTDWrapper.baseWrapper == aDTDWrapper) ?
                  (aDTDWrapper.wrappedSource) :
                  vbUtils.getDTDFragment(aDTDWrapper.baseWrapper.wrappedSource);
    assert(doctype, "No document type?");
    var entityValueArray = aDTDWrapper.getInternalEntityNames(aCount);
    var entityMap = {};
    for (i = 0; i < aCount.value; i++) {
      var entityName  = entityValueArray[i];
      var entityValue = aDTDWrapper.getEntityValue(entityName);
      var entity = new Entity(aDTDWrapper,
                              entityName,
                              entityValue,
                              doctype,
                              this,
                              null);
      entityMap[entityName] = entity;
    }

    this._mappedDTDs[uri] = {
      wrapper: aDTDWrapper,
      entityMap: entityMap
    };
  },

  /**
   * Reset an Entity object to its initial state.
   *
   * @param aEntity The entity to reset.
   */
  resetEntity: function resetEntity(aEntity) {
    assert(aEntity instanceof Entity, "Not an entity?");
    try {
      aEntity._walker.currentNode = aEntity._fragment;
      aEntity.status = ENTITY_STATUS.NO_MATCH;
    } catch (e) {
      dump(e.stack + "\n");
      throw e;
    }
  },

  /**
   * Insert an entity reference node around a range.
   *
   * @param aEntityName The name of the entity reference.
   * @param aRange      The range to surround with the entity reference.
   *
   * @return Entity reference DOM node.
   */
  insertEntityRef: function insertEntityRef(aEntityName, aRange) {
    var doc = aRange.startContainer.ownerDocument;
    /* XXX ajvincent We fudge this, because the native Mozilla DOM doesn't
       support entity references very well, if at all.  This creates a
       <markup:entity-ref/> element instead.
    */
    var entityRef = doc.createElementNS(MARKUP_NS, "markup:entity-ref");
    entityRef.setAttribute("name", aEntityName);

    aRange.surroundContents(entityRef);
    this._entitiesReplaced++;

    return entityRef;
  },

  /**
   * Get the entities which apply for a given XML document wrapper.
   *
   * @param aWrapper The XMLDocumentWrapper to find entities for.
   *
   * @return JSObject mapping all DTD entities not overridden for a document.
   */
  getEntityMap: function getEntityMap(aWrapper) {
    assert(aWrapper instanceof xeIXMLDocumentWrapper,
           "Not a XML document wrapper?");
    var map = {};
    var wrappersList = [aWrapper.subsetWrapper];

    // Get external DTD wrappers first.
    for (var j = 0; j < wrappersList.length; j++) {
      var aCount = {};
      var i;
      var subWrappers = wrappersList[j].getExternalDTDWrappers(aCount);
      var origCount = 0;
      if (getDebugPref()) {
        origCount = wrappersList.length;
      }
      wrappersList = wrappersList.concat(subWrappers);
      if (getDebugPref() && (origCount > 1)) {
        assert(wrappersList.length == origCount + aCount.value, "Length oopsie!");
      }
    }

    // Process the DTD wrappers in reverse order.  Parent DTD's take precedence.
    for (j = wrappersList.length - 1; j >= 0; j--) {
      assert(wrappersList[j] instanceof xeIDTDWrapper,
             "Not a DTD wrapper?");
      assert(wrappersList[j].baseWrapper instanceof xeIDocumentWrapper,
             "Not a document wrapper?");
      var uri = wrappersList[j].baseWrapper.publicURI;
      assert(uri in this._mappedDTDs, "Not mapped???");
      var submap = this._mappedDTDs[uri].entityMap;
      for (var prop in submap) {
        map[prop] = submap[prop];
      }
    }

    return map;
  },

  /**
   * Compare a document wrapper's node with a matching DTD entity's node.
   *
   * @param aNode The node from the document wrapper.
   * @param aMatchNode The node from the entity.
   * @param aDataPosition DATA_MATCH code where aMatchNode must be in aNode.
   *
   * @return One of the ENTITY_STATUS codes.
   */
  compareEntityNode: function compareEntityNode(aNode, aMatchNode, aDataPosition) {
    try {
      assert(aNode instanceof nsIDOMNode, "Not a DOM node?");
      assert(aMatchNode instanceof nsIDOMNode, "Not a DOM node?");
    } catch (e) {
      dump(e.stack + "\n");
      throw e;
    }

    /* XXX ajvincent It would be really nice to use nsIDOM3Node.isEqualNode,
       except in most cases that's guaranteed not to work, due to differing
       namespaces between the two nodes.
    */

    var rv = ENTITY_STATUS.PARTIAL_MATCH;
    if (aNode.nodeType != aMatchNode.nodeType) {
      return ENTITY_STATUS.NO_MATCH;
    }
    if (aNode.localName != aMatchNode.localName) {
      return ENTITY_STATUS.NO_MATCH;
    }
    if (aNode.childNodes.length != aMatchNode.childNodes.length) {
      return ENTITY_STATUS.NO_MATCH;
    }

    var i;
    if (aNode.nodeType == nsIDOMNode.ELEMENT_NODE) {
      // Check attributes.
      if (aNode.attributes.length != aMatchNode.attributes.length) {
        return ENTITY_STATUS.NO_MATCH;
      }
      for (i = 0; i < aNode.attributes.length; i++) {
        var attrNode = aNode.attributes[i];
        var matchAttrNode = aMatchNode.getAttributeNodeNS(attrNode.namespaceURI,
                                                          attrNode.localName);
        if (!matchAttrNode) {
          return ENTITY_STATUS.NO_MATCH
        }
        rv = this.compareEntityNode(attrNode, matchAttrNode, DATA_MATCH.FULL);

        if (rv == ENTITY_STATUS.NO_MATCH) {
          return rv;
        }
      }
    }

    if (aNode instanceof nsIDOMCharacterData) {
      assert(aMatchNode instanceof nsIDOMCharacterData, "Not a data node?");
      switch (aDataPosition) {
        case DATA_MATCH.START:
          if (aNode.data.indexOf(aMatchNode.data) != 0) {
            return ENTITY_STATUS.NO_MATCH;
          }
          break;

        case DATA_MATCH.FULL:
          if (aNode.data != aMatchNode.data) {
            return ENTITY_STATUS.NO_MATCH;
          }
          break;

        case DATA_MATCH.END:
          if (aNode.data.indexOf(aMatchNode.data) !=
              aNode.data.length - aMatchNode.data.length) {
            return ENTITY_STATUS.NO_MATCH;
          }
          break;

        default:
          throw Components.results.NS_ERROR_INVALID_ARG;
      }
    }

    // Check child nodes.
    for (i = 0; i < aNode.childNodes.length; i++) {
      rv = this.compareEntityNode(aNode.childNodes[i],
                                  aMatchNode.childNodes[i],
                                  DATA_MATCH.FULL);
      if (rv == ENTITY_STATUS.NO_MATCH) {
        return rv;
      }
    }

    return rv;
  },


  /**
   * Persist the value of an entity across all affected documents.
   *
   * @param aEntity Entity object to persist.
   */
  persistValue: function persistValue(aEntity) {
    this.checkInitialized();

    // First, get the wrapper the entity belongs to.
    var wrapper = null;
    var packLength = this._docPack.length;
    assert(aEntity._ownerNode, "No owner node?");
    var targetDoc = aEntity._ownerNode.ownerDocument;

    for (var i = 0; i < packLength; i++) {
      wrapper = this._docPack.item(i);
      if ((wrapper instanceof xeIXMLDocumentWrapper) &&
          (wrapper.wrappedDocument == targetDoc)) {
        break;
      }
      wrapper = null;
    }
    assert(wrapper, "No matching wrapper found?");

    // Get the list of clones this entity belongs to.
    var entityMap = this.getEntityMap(wrapper);
    var entityList = entityMap[aEntity.name]._cloneList;
    var i;

    // A few debug checks for good measure.
    if (getDebugPref()) {
      assert(entityList instanceof Array, "Not an array?");
      assert(entityList.length >= 2, "Where is aEntity in entityList?");
      var found = false;
      for (i = 0; i < entityList.length; i++) {
        assert(entityList[i] instanceof Entity);
        if (aEntity == entityList[i]) {
          found = true;
        }
      }
      assert(found, "aEntity not in entityList?");

      for (i = 1; i < entityList.length; i++) {
        assert(entityList[i]._ownerNode,
               "Missing an owner node?");
      }
      assert(typeof entityList[0]._ownerNode == "undefined",
             "Unexpected owner node?");
    }

    // Set the entity's new value on the DOM node(s) it affects.
    dump('original entity: ' + aEntity + '\n');
    var attrList = [];
    for (i = 1; i < entityList.length; i++) {
      if (entityList[i]._ownerNode instanceof nsIDOMAttr) {
        var attrNode = entityList[i]._ownerNode;

        // Make sure we haven't touched this attribute yet.
        var matchFound = false;
        for (j = 0; j < attrList.length; j++) {
          if (attrList[j] == attrNode) {
            matchFound = true;
            break;
          }
        }
        if (matchFound) {
          continue;
        }
        attrList.push(attrNode);

        var index = attrNode.nodeValue.indexOf(entityList[i]._storedSource);
        if (index > -1) {
          var endIndex = index + entityList[i]._storedSource.length;
          var part1 = attrNode.nodeValue.substr(0, index);
          var part3 = attrNode.nodeValue.substr(endIndex);

          attrNode.nodeValue = part1 + aEntity.source + part3;
          dump("new attrNode.nodeValue: " + attrNode.nodeValue + "\n");
        } else {
          warn("attribute node doesn't have entity stored source in it... why?");
        }
      } else {
        NOT_IMPLEMENTED();
      }
    }

    // Update the source code behind the entity in the DTD.
    for (i = 0; i < entityList.length; i++) {
      entityList[i].source = aEntity.source;
      entityList[i]._storedSource = aEntity.source;
    }
    var base = entityList[0];
    base.ownerDTD.setEntityValue(base.name, base.source);
  },

  // xeIEntityManager
  initialize: function initialize(aDocPack) {
    this.checkNotInitialized();
    this._docPack = aDocPack;
  },

  // xeIEntityManager
  mapEntities: function mapEntities(aWrapper) {
    this.checkInitialized();
    // We only support XML wrappers and DTD document wrappers.
    var dtdWrapper = null;
    if (aWrapper instanceof xeIXMLDocumentWrapper) {
      dtdWrapper = aWrapper.subsetWrapper;
    }
    else if ((aWrapper instanceof xeIDTDWrapper) &&
             (aWrapper instanceof xeIDocumentWrapper)) {
      dtdWrapper = aWrapper;
    }
    else {
      throw Components.results.NS_ERROR_INVALID_ARG;
    }

    // (1) Get the entities in the DTD internal subset.
    this.mapDTD(dtdWrapper);
    dump("Done mapping DTD's\n");

    /* The rest of this code concerns itself with mapping to a XML document.
       So we really don't want to continue if the wrapper is a DTD wrapper.
    */
    if (!(aWrapper instanceof xeIXMLDocumentWrapper)) {
      return;
    }

    var entityMap = this.getEntityMap(aWrapper);

    // (2) Create XML fragments for each entity.
    var doctype = vbUtils.getDTDFragment(aWrapper.wrappedSource);
    for each (var entity in entityMap) {
      if (entity._fragment) {
        assert(entity._fragment.firstChild, "No child node?");
        assert(entity._fragment instanceof nsIDOMDocumentFragment,
               "Not a document fragment?");
        var doc = entity._ownerDoc;
        assert(doc == entity._fragment.ownerDocument,
               "Missing owner document!");
        assert(doc, "How was this fragment created???");
        entity._walker = doc.createTreeWalker(
                           entity._fragment,
                           nsIDOMNodeFilter.SHOW_ALL,
                           null,
                           true
                         );
      }
    }
    dump("Done creating XML fragments\n");

    // (3) Sort the fragments by entity value length and fragment contents.
    var dataEntities = [];
    var nodeEntities = [];
    for (entityName in entityMap) {
      var targetArray = (entityMap[entityName].safeForAttributes) ?
                        dataEntities :
                        nodeEntities;
      targetArray[targetArray.length] = entityName;
    }

    var i;
    var j;

    // Debug check:  make sure all text entities hold a single text node.
    if (getDebugPref()) {
      for (i = 0; i < dataEntities.length; i++) {
        entity = entityMap[dataEntities[i]];
        assert(entity._fragment.childNodes.length == 1, "Expect a single text!");
        assert(entity._fragment.firstChild.nodeType == nsIDOMNode.TEXT_NODE,
               "Expect a single text!");
      }
    }

    dataEntities.sort(function sortDataEntities(a, b) {
      var aEntity = entityMap[a].source;
      var bEntity = entityMap[b].source;
      if (aEntity < bEntity) {
        return +1;
      }
      if (bEntity < aEntity) {
        return -1;
      }
      return 0;
    });

    nodeEntities.sort(function sortNodeEntities(a, b) {
      function sortFragments(fragA, fragB) {
        if (fragA.childNodes.length < fragB.childNodes.length) {
          return +1;
        }
        if (fragB.childNodes.length < fragA.childNodes.length) {
          return -1;
        }

        if (fragA.nodeType < fragB.nodeType) {
          return +1;
        }
        if (fragB.nodeType < fragA.nodeType) {
          return -1;
        }

        if (fragA.localName < fragB.localName) {
          return +1;
        }
        if (fragB.localName < fragA.localName) {
          return -1;
        }

        if (fragA.nodeType == nsIDOMNode.ELEMENT_NODE) {
          if (fragA.attributes.length < fragB.attributes.length) {
            return +1;
          }
          if (fragB.attributes.length < fragA.attributes.length) {
            return -1;
          }
        }

        for (var i = 0; i < fragA.childNodes.length; i++) {
          var rv = sortFragments(fragA.childNodes[i], fragB.childNodes[i]);
          if (rv != 0) {
            return rv;
          }
        }
        return 0;
      }

      assert(a in entityMap, "Missing entity!");
      assert(b in entityMap, "Missing entity!");
      return sortFragments(entityMap[a]._fragment,
                           entityMap[b]._fragment);
    });
    dump("Done sorting fragments\n");

    /*
    (4) Walk the DOM tree of the document wrapper.  As an entity match is
        found, replace the match with <markup:entityref name="entityName"/>
        and move the match into the reference element's anonymous content.
    (5) While walking, look out for attributes containing text snippets
        of qualifying entities.
    */
    for (i = 0; i < dataEntities.length; i++) {
      this.resetEntity(entityMap[dataEntities[i]]);
    }
    for (i = 0; i < nodeEntities.length; i++) {
      this.resetEntity(entityMap[nodeEntities[i]]);
    }

    var doc = aWrapper.wrappedDocument;
    var range;

    // Attribute entity handling.
    var walker = doc.createTreeWalker(
                  doc,
                  nsIDOMNodeFilter.SHOW_ELEMENT,
                  null,
                  true);
    while (walker.nextNode()) {
      var attrs = walker.currentNode.attributes;
      for (i = 0; i < attrs.length; i++) {
        var entityMatches = null;

        for (j = 0; j < dataEntities.length; j++) {
          entity = entityMap[dataEntities[j]];
          assert(entity instanceof Entity, "Internal corruption!!");
          var entitySource = entity.source;
          var entityIndex = -1;
          if (entitySource.length > 5) {
            // Long entity values, we want a partial match.
            entityIndex = attrs[i].nodeValue.indexOf(entitySource);
          } else {
            // Short entities, we want an exact match.
            if (attrs[i].nodeValue == entitySource) {
              entityIndex = 0;
            }
          }
          if (entityIndex != -1) {
            if (!entityMatches) {
              entityMatches = [];
            }
            entity = entity.clone();
            entity._ownerNode = attrs[i];
            entityMatches.push(entity);
          }
        } // end for (j = 0; j < dataEntities.length; j++)

        if (entityMatches) {
          assert(attrs[i] instanceof nsIDOM3Node, "Not a DOM node?");
          attrs[i].setUserData("entityMatches",
                               ArrayConverter.nsIArray(entityMatches),
                               null);
        }
      } // end (for i = 0; i < attrs.length; i++)
    } // end while (walker.nextNode())

    var entityFilter = {
      ancestorNode: null,
      acceptNode: function acceptNode(aNode) {
        if (!this.ancestorNode) {
          return nsIDOMNodeFilter.FILTER_ACCEPT;
        }
        var filter = (this.ancestorNode.compareDocumentPosition(aNode) &
                      nsIDOM3Node.DOCUMENT_POSITION_CONTAINED_BY) ?
                      nsIDOMNodeFilter.FILTER_REJECT :
                      nsIDOMNodeFilter.FILTER_ACCEPT;
        return filter;
      }
    }

    walker = doc.createTreeWalker(
              doc,
              nsIDOMNodeFilter.SHOW_ALL,
              entityFilter,
              true);
    var matchNode = null;

    while (walker.nextNode()) {
      var node = walker.currentNode;
      for (i = 0; i < nodeEntities.length; i++) {
        entity = entityMap[nodeEntities[i]];
        assert(entity.status == ENTITY_STATUS.NO_MATCH, "Should be fresh!");
        matchNode = entity._walker.nextNode();
        assert(matchNode == entity._fragment.firstChild, "Didn't we reset this?");
        entity._status = this.compareEntityNode(node, matchNode, DATA_MATCH.END);

        if (entity._status == ENTITY_STATUS.PARTIAL_MATCH) {
          startNode = walker.currentNode;
          var walkerSibling = startNode;
          while (matchNode.nextSibling) {
            matchNode = matchNode.nextSibling;

            var dataMatch = (matchNode.nextSibling) ?
                            DATA_MATCH.FULL :
                            DATA_MATCH.START;
            walkerSibling = walkerSibling.nextSibling;

            if (!walkerSibling) {
              entity._status = ENTITY_STATUS.NO_MATCH;
              break;
            }

            entity._status = this.compareEntityNode(walkerSibling,
                                                    matchNode,
                                                    dataMatch);

            if (entity._status == ENTITY_STATUS.NO_MATCH) {
              break;
            }
          }

          if (entity._status == ENTITY_STATUS.NO_MATCH) {
            walker.currentNode = startNode;
            this.resetEntity(entityMap[nodeEntities[i]]);
            continue;
          }
          assert(entity._status == ENTITY_STATUS.PARTIAL_MATCH, "Check status!");
          entity._status = ENTITY_STATUS.COMPLETE_MATCH; // Yay!

          range = doc.createRange();
          if (entity._fragment.firstChild instanceof nsIDOMCharacterData) {
            var startFragment = entity._fragment.firstChild;
            range.setStart(node, node.data.indexOf(startFragment.data));
          } else {
            range.setStartBefore(node);
          }

          if (entity._fragment.lastChild instanceof nsIDOMCharacterData) {
            assert(walkerSibling instanceof nsIDOMCharacterData, "We're lost!");
            var endData = entity._fragment.lastChild.data;
            range.setEnd(walkerSibling,
                         walkerSibling.data.indexOf(endData) + 1);
          } else {
            range.setEndAfter(walkerSibling);
          }

          var refNode = this.insertEntityRef(nodeEntities[i], range);
          assert(refNode, "No ref node???");
          walker.currentNode = refNode;
          entityFilter.ancestorNode = refNode;
        }

        this.resetEntity(entityMap[nodeEntities[i]]);
      } // end for (i = 0; i < nodeEntities.length; i++)
    } // end while (walker.nextNode());

    if ((this._entitiesReplaced == 0) &&
        (nodeEntities.length + dataEntities.length > 0)) {
      warn("Didn't we do anything???");
    }
  },

  // xeIEntityManager
  getSanitizedContent: function getSanitizedContent(aWrapper) {
    this.checkInitialized();
    // First, place the original doctype where it belongs.
    // XXX ajvincent This doctype handling should be in verbosio-utils.js.
    var source = aWrapper.wrappedSource;
    var dtdFragment = vbUtils.getDTDFragment(source);
    if (dtdFragment) {
      var dtdIndex = source.indexOf(dtdFragment);
      var firstPart = source.substr(0, dtdIndex);
      var thirdPart = source.substr(dtdIndex + dtdFragment.length);
      source = firstPart + aWrapper.documentType + thirdPart;
    }
    var entityMap = this.getEntityMap(aWrapper);

    // Take care of text entities in strings.
    // XXX ajvincent This will be a problem for CDATA sections...
    for (var entityName in entityMap) {
      var entity = entityMap[entityName];

      assert(entity instanceof Entity, "Internal corruption!!");
      if (!entity._fragment) {
        continue;
      }
      if (entity._fragment.childNodes > 1) {
        continue;
      }
      if (entity._fragment.firstChild.nodeType != nsIDOMNode.TEXT_NODE) {
        continue;
      }

      // Okay, we should have just the text entities now.
      var parsedSource = source;
      var startIndex = parsedSource.indexOf(entity.source);
      while (startIndex != -1) {
        var quoteChar1 = parsedSource.charAt(startIndex - 1);
        if ((quoteChar1 != "'") && (quoteChar1 != '"')) {
          startIndex = parsedSource.indexOf(entity.source, startIndex + 1);
          continue;
        }

        var quoteChar2 = parsedSource.charAt(startIndex + entity.source.length);
        if (quoteChar1 != quoteChar2) {
          startIndex = parsedSource.indexOf(entity.source, startIndex + 1);
          continue;
        }

        firstPart = parsedSource.substr(0, startIndex);
        thirdPart = parsedSource.substr(startIndex + entity.source.length);
        parsedSource = firstPart + "&" + entityName + ";" + thirdPart;
        startIndex = parsedSource.indexOf(entity.source,
                                          startIndex + entityName.length + 2);
      }
      source = parsedSource;
    }

    // Take care of the XML fragments.
    const closeEntity = "</markup:entity-ref>";
    const startEntity = "<markup:entity-ref ";

    var closeEntityIndex = source.indexOf(closeEntity);
    while (closeEntityIndex != -1) {
      var sourceFragment = source.substr(0, closeEntityIndex);
      var startEntityIndex = sourceFragment.lastIndexOf(startEntity);
      assert(startEntityIndex != -1, "Where's the start of the entity ref?");
      closeEntityIndex += closeEntity.length;

      var firstPart = source.substr(0, startEntityIndex);
      var thirdPart = source.substr(closeEntityIndex);
      const nameRE = /name\=\"([^\"]*)\"/;
      var name = nameRE.exec(source)[1];
      assert(name in entityMap, name + " entity not in map?");
      source = firstPart + "&" + name + ";" + thirdPart;

      closeEntityIndex = source.indexOf(closeEntity);
    }

    return source;
  },

  _interfaces: [xeIEntityManager,
                nsIClassInfo],

  // nsIClassInfo
  getInterfaces: ArrayConverter.xpcomArrayMethod("_interfaces"),

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

  // nsIClassInfo
  classDescription: "Verbosio DTD entity manager",

  // nsIClassInfo
  classID: Components.ID("{d305bd42-ff10-4a2e-8b97-a544ae6ff0af}"),

  // nsIClassInfo
  contractID: "@verbosio.mozdev.org/dtd-entity-manager;1",

  // nsIClassInfo
  implementationLanguage: nsIProgrammingLanguage.JAVASCRIPT,

  // nsIClassInfo
  flags: 0, /* nsIClassInfo.SINGLETON for services */

  // nsISupports
  QueryInterface: function QueryInterface(aIID) {
    for (var j = 0; j < this._interfaces.length; j++) {
      if (aIID.equals(this._interfaces[j])) {
        return this;
      }
    }

    if (aIID.equals(nsISupports))
      return this;

    // Known interfaces we don't support.
    if (aIID.equals(nsISecurityCheckedComponent)) {
      throw Components.results.NS_ERROR_NO_INTERFACE;
      return null;
    }

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

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

var NSGetModule = XPCOMUtils.generateNSGetModule([
  EntityManager
]);
