const domTreeViewMaker = {
  /**
   * Recursively generate tree items for a given DOM node and a treechildren element.
   *
   * @param aTreeChildren    <xul:treechildren/> element to append children to.
   * @param aNode            DOM node to use as reference for tree item.
   * @param treeitemTemplate <xul:treeitem/> to use as a template.
   * @param beforeItem       Item to insert the newly created item before.
   * @param aDepth           Depth to open grandchildren elements.
   */
  addToChildren: function addToChildren(aTreeChildren,
                                        aNode,
                                        treeitemTemplate,
                                        beforeItem,
                                        aDepth)
  {
    var treeitem = treeitemTemplate.cloneNode(true);
    treeitem.ownerTree = treeitemTemplate.ownerTree;
    treeitem.setUserData("referenceNode", aNode, null);
    /* XXX ajvincent This is not scalable; we need some other way to tie tree
       items to nodes (for example, when a node has more than one tree item,
       such as for a XML document wrapper and a XML fragment wrapper.)
     */
    aNode.setUserData("treeitemNode", treeitem, null);
    var cell = treeitem.firstChild.firstChild;

    var cols = treeitemTemplate.ownerTree.propertyCols;
    var i;
    if ((aNode.namespaceURI == MARKUP_NS) && (aNode.localName == "entity-ref")) {
      // <markup:entity-ref name="foo"/> -> &foo;
      const nsIDOMNode = Components.interfaces.nsIDOMNode;
      for (i = 0; i < cols.length; i++)
      {
        if (!cols[i])
          continue;
        if (!aNode[cols[i]])
          continue;
        var cellLabel = aNode[cols[i]];
        switch (cols[i]) {
          case "namespaceURI":
            cellLabel = "";
            break;

          case "nodeName":
          case "localName":
            cellLabel = aNode.getAttribute("name");
            break;

          case "nodeType":
            cellLabel = nsIDOMNode.ENTITY_REFERENCE_NODE;
            break;
        }
        cell.setAttribute("label", cellLabel);
        cell = cell.nextSibling;
      }
    } else {
      for (i = 0; i < cols.length; i++)
      {
        if (!cols[i])
          continue;
        if (!aNode[cols[i]])
          continue;
        cell.setAttribute("label", aNode[cols[i]]);
        cell = cell.nextSibling;
      }
    }

    if (aNode.hasChildNodes())
    {
      treeitem.setAttribute("container", "true");
      if (aDepth > 0)
      {
        treeitem.setAttribute("open", "true");

        for (i = 0; i < aNode.childNodes.length; i++)
        {
          var child = aNode.childNodes[i];
          this.addToChildren(treeitem.lastChild, child, treeitemTemplate, null, aDepth - 1);
        }
      }
    }

    try
    {
      aTreeChildren.insertBefore(treeitem, beforeItem);
    }
    catch (e)
    {
      debugger;
    }
  },

  isReplacingChildIndex: -1,

  /**
   * Update the target node's tree item for the inserted child.
   */
  onNodeInserted: function onNodeInserted(evt)
  {
    var parentItem = evt.relatedNode.getUserData("treeitemNode");
    if (!parentItem)
      return;

    var valuePanel = parentItem.ownerTree.valuePanel;
    var parentItemChildren = parentItem.lastChild;

    var nextNode = evt.target.nextSibling ?
                   evt.target.nextSibling.getUserData("treeitemNode") :
                   null;
    if (evt.target.getUserData("treeitemNode"))
    {
      // Good news.  We've already done the work.
      parentItemChildren.insertBefore(evt.target.getUserData("treeitemNode"), nextNode);
    } else {
      domTreeViewMaker.addToChildren(parentItemChildren,
                                     evt.target,
                                     parentItem.ownerTree.getUserData("itemTemplate"),
                                     nextNode);
    }

    if (valuePanel &&
        (typeof valuePanel.DOMNodeInserted == "function"))
      valuePanel.DOMNodeInserted(evt);

    if (this.isReplacingChildIndex != -1)
    {
      parentItem.ownerTree.contentView.selection.select(this.isReplacingChildIndex);
      this.isReplacingChildIndex = -1;
    }
  },

  /**
   * Update the target node's tree item for the removed child.
   */
  onNodeRemoved: function onNodeRemoved(evt)
  {
    var parentItem = evt.relatedNode.getUserData("treeitemNode");
    if (!parentItem)
      return;

    var parentItemChildren = parentItem.lastChild;

    // Were we called by replaceChild?
    try {
      throw new Error("foo");
    }
    catch (e) {
      var stackLine = e.stack.split("\n")[2];
      if (stackLine.indexOf("replaceChild") == 0)
        this.isReplacingChildIndex = parentItem.ownerTree.currentIndex;
    }

    var valuePanel = parentItem.ownerTree.valuePanel;
    if (valuePanel &&
        (typeof valuePanel.DOMNodeRemoved == "function"))
      valuePanel.DOMNodeRemoved(evt);

    parentItemChildren.removeChild(evt.target.getUserData("treeitemNode"));
  },

  /**
   * Add tree items where the tree needs them.
   *
   * @param evt DOM event targeted at the tree item being opened.
   */
  onOpenRow: function onOpenRow(evt) {
    if (evt.attrName != "open")
      return;

    if (evt.newValue != "true")
      return;

    var treeitem = evt.target;
    if (treeitem.lastChild.firstChild) {
      return;
    }

    var node = treeitem.getUserData("referenceNode");
    assert(node, "No reference node?");
    var treeitemTemplate = evt.currentTarget.getUserData("itemTemplate");
    for (var i = 0; i < node.childNodes.length; i++)
    {
      var child = node.childNodes[i];
      domTreeViewMaker.addToChildren(treeitem.lastChild, child, treeitemTemplate, null, 0);
    }
  },

  addListeners: function addListeners(aNode, aTree)
  {
    aNode.addEventListener("DOMNodeInserted", this.onNodeInserted, false);
    aNode.addEventListener("DOMNodeRemoved", this.onNodeRemoved, false);
    aTree.addEventListener("DOMAttrModified", this.onOpenRow, true);
  },

  removeListeners: function removeListeners(aNode, aTree)
  {
    aNode.removeEventListener("DOMNodeInserted", this.onNodeInserted, false);
    aNode.removeEventListener("DOMNodeRemoved", this.onNodeRemoved, false);
    aTree.removeEventListener("DOMAttrModified", this.onOpenRow, true);
  },

  /**
   * Refresh a tree with a given template and a new root node.
   *
   * @param tree
   * @param rootNode
   */
  refreshTree: function refreshTree(aTree, rootNode)
  {
    // Remove event listeners.
    this.removeListeners(rootNode, aTree);

    const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

    // Clear all child items
    var treechildren = aTree.lastChild;
    while (treechildren.localName != "treechildren")
      treechildren = treechildren.previousSibling;
    while (treechildren.firstChild)
    {
      var item = treechildren.firstChild;
      var refNode = item.getUserData("referenceNode");
      if (refNode)
      {
        this.removeListeners(refNode, aTree);
      }
      treechildren.removeChild(item);
    }

    // If we've created the item node before, just go with it.
    if (rootNode.getUserData("treeitemNode"))
    {
      treechildren.appendChild(rootNode.getUserData("treeitemNode"));
      this.addListeners(rootNode, aTree);
      return;
    }

    // Determine if we need to create a treeitem template.
    var addCol = (typeof aTree.itemTemplate != "object");
    if (addCol)
    {
      var template = document.createElementNS(XUL_NS, "treeitem");
      var treerow = document.createElementNS(XUL_NS, "treerow");
      template.appendChild(treerow);
      var templateChildren = document.createElementNS(XUL_NS, "treechildren");
      template.appendChild(templateChildren);
      aTree.setUserData("itemTemplate", template, null);
      template.ownerTree = aTree;
    }

    // Determine columns to insert node properties in.
    var cols = [];
    var currentCol = aTree.columns.getFirstColumn();
    while (currentCol)
    {
      cols[cols.length] = currentCol.element.getAttribute("propertyname");
      if (addCol)
      {
        var treecell = document.createElementNS(XUL_NS, "treecell");
        treerow.appendChild(treecell);
      }
      currentCol = currentCol.getNext();
    }

    if (typeof aTree.propertyCols == "undefined")
      aTree.propertyCols = cols;

    // Generate the tree's descendants.
    this.addToChildren(treechildren, rootNode, template, null, 2);
    assert(rootNode.getUserData("treeitemNode"), "No tree item node?");

    // Set the value panel.

    if (typeof aTree.valuePanel != "object")
    {
      var valuePanelId = aTree.getAttribute("valuepanel");
      aTree.valuePanel = valuePanelId ? aTree.ownerDocument.getElementById(valuePanelId) : null;
    }

    // Add event listeners.
    this.addListeners(rootNode, aTree);
    aTree.eventLocks = 0;
  }
};
