/* ***** 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 mozilla.org code.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 2001
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Joe Hewitt <hewitt@netscape.com> (original author)
 *   Jason Barnabe <jason_barnabe@fastmail.fm>
 *   Shawn Wilsher <me@shawnwilsher.com>
 *
 * 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 ***** */

/***************************************************************
* DOMViewer --------------------------------------------
*  Views all nodes within a document.
****************************************************************/

//////////// global variables /////////////////////

var viewer;

//////////// global constants ////////////////////

const kDOMViewCID          = "@mozilla.org/inspector/dom-view;1";
const kClipboardHelperCID  = "@mozilla.org/widget/clipboardhelper;1";
const kPromptServiceCID    = "@mozilla.org/embedcomp/prompt-service;1";
const nsIDOMNode           = Components.interfaces.nsIDOMNode;
const nsIDOMElement        = Components.interfaces.nsIDOMElement;

//////////////////////////////////////////////////

window.addEventListener("load", DOMViewer_initialize, false);
window.addEventListener("unload", DOMViewer_destroy, false);

function DOMViewer_initialize()
{
  viewer = new DOMViewer();
  viewer.initialize(parent.FrameExchange.receiveData(window));
}

function DOMViewer_destroy()
{
  PrefUtils.removeObserver("inspector", PrefChangeObserver);
  viewer.removeClickListeners();
  viewer = null;
}

////////////////////////////////////////////////////////////////////////////
//// class DOMViewer 

function DOMViewer() // implements inIViewer
{
  this.mObsMan = new ObserverManager(this);
    
  this.mDOMTree = document.getElementById("trDOMTree");
  this.mDOMTreeBody = document.getElementById("trDOMTreeBody");

  // prepare and attach the DOM DataSource
  this.mDOMView = XPCU.createInstance(kDOMViewCID, "inIDOMView");
  this.mDOMView.showSubDocuments = true;
  this.mDOMView.whatToShow &= ~(NodeFilter.SHOW_ATTRIBUTE); // hide attribute nodes
  this.mDOMTree.treeBoxObject.view = this.mDOMView;

  PrefUtils.addObserver("inspector", PrefChangeObserver);
}

DOMViewer.prototype = 
{
  mSubject: null,
  mDOMView: null,
  // searching stuff
  mSearchResults: null,
  mSearchCurrentIdx: null,
  mSearchDirection: null,
  mColumns: null,
  mFlashSelected: null,
  mFlashes: 0,
  mFindDir: null,
  mFindParams: null,
  mFindType: null,
  mFindWalker: null,
  mSelecting: false,
  
  ////////////////////////////////////////////////////////////////////////////
  //// interface inIViewer

  //// attributes 

  get uid() { return "dom" },
  get pane() { return this.mPanel },
  get editable() { return true; },
  
  get selection() { return this.mSelection },

  get subject() { return this.mSubject },
  set subject(aObject) {
    this.mSubject = aObject;
    this.mDOMView.rootNode = aObject;
    this.mObsMan.dispatchEvent("subjectChange", { subject: aObject });
    this.setInitialSelection(aObject);
  },

  //// methods

  /**
   * Properly sets up the DOM Viewer
   *
   * @param aPane The panel this references.
   */
  initialize: function initialize(aPane)
  {
    //this.initColumns();

    this.mPanel = aPane;

    this.setAnonContent(PrefUtils.getPref("inspector.dom.showAnon"));
    this.setProcessingInstructions(PrefUtils.getPref("inspector.dom.showProcessingInstructions"));
    this.setAccessibleNodes(PrefUtils.getPref("inspector.dom.showAccessibleNodes"));
    this.setWhitespaceNodes(PrefUtils.getPref("inspector.dom.showWhitespaceNodes"));
    this.setFlashSelected(PrefUtils.getPref("inspector.blink.on"));

    aPane.notifyViewerReady(this);
  },

  destroy: function()
  {
    this.mDOMTree.treeBoxObject.view = null;
  },

  isCommandEnabled: function(aCommand)
  {
    var clipboardNode = null;
    var selectedNode = null;
    var parentNode = null;
    if (/^cmdEditPaste/.test(aCommand)) {
      if (this.mPanel.panelset.clipboardFlavor != "inspector/dom-node")
        return false;
      clipboardNode = this.mPanel.panelset.getClipboardData();
    }
    if (/^cmdEdit(Paste|Insert)/.test(aCommand)) {
      selectedNode = new XPCNativeWrapper(viewer.selectedNode, "nodeType",
                                          "parentNode", "childNodes");
      if (selectedNode.parentNode)
        parentNode = new XPCNativeWrapper(selectedNode.parentNode, "nodeType");
    }
    switch (aCommand) {
      case "cmdEditPaste":
      case "cmdEditPasteBefore":
        return this.isValidChild(parentNode, clipboardNode);
      case "cmdEditPasteReplace":
        return this.isValidChild(parentNode, clipboardNode, selectedNode);
      case "cmdEditPasteFirstChild":
      case "cmdEditPasteLastChild":
        return this.isValidChild(selectedNode, clipboardNode);
      case "cmdEditPasteAsParent":
        return this.isValidChild(clipboardNode, selectedNode) &&
               this.isValidChild(parentNode, clipboardNode, selectedNode);
      case "cmdEditInsertAfter":
      	return parentNode instanceof nsIDOMElement;
      case "cmdEditInsertBefore":
      	return parentNode instanceof nsIDOMElement;
      case "cmdEditInsertFirstChild":
      	return selectedNode instanceof nsIDOMElement;
      case "cmdEditInsertLastChild":
      	return selectedNode instanceof nsIDOMElement;
      case "cmdEditCut":
      case "cmdEditCopy":
      case "cmdEditDelete":
        return this.selectedNode != null;
    }
    return false;
  },

 /**
  * Determines whether the passed parent/child combination is valid.
  * @param the parent
  * @param the child
  * @param the node the child is replacing (optional)
  * @return true if the passed parent can have the passed child as a child,
  *   false otherwise
  */
  isValidChild: function isValidChild(parent, child, replaced)
  {
    // the document (fragment) node must be an only child and can't be replaced
    if (parent == null)
      return false;
    // the only types that can ever have children
    if (parent.nodeType != nsIDOMNode.ELEMENT_NODE &&
        parent.nodeType != nsIDOMNode.DOCUMENT_NODE &&
        parent.nodeType != nsIDOMNode.DOCUMENT_FRAGMENT_NODE)
      return false;
    // the only types that can't ever be children
    if (child.nodeType == nsIDOMNode.DOCUMENT_NODE ||
        child.nodeType == nsIDOMNode.DOCUMENT_FRAGMENT_NODE ||
        child.nodeType == nsIDOMNode.ATTRIBUTE_NODE)
      return false;
    // doctypes can only be the children of documents
    if (child.nodeType == nsIDOMNode.DOCUMENT_TYPE_NODE &&
        parent.nodeType != nsIDOMNode.DOCUMENT_NODE)
      return false;
    // only elements and fragments can have text, cdata, and entities as
    // children
    if (parent.nodeType != nsIDOMNode.ELEMENT_NODE &&
        parent.nodeType != nsIDOMNode.DOCUMENT_FRAGMENT_NODE && 
       (child.nodeType == nsIDOMNode.TEXT_NODE ||
        child.nodeType == nsIDOMNode.CDATA_NODE ||
        child.nodeType == nsIDOMNode.ENTITY_NODE))
      return false;
    // documents can only have one document element or doctype
    if (parent.nodeType == nsIDOMNode.DOCUMENT_NODE &&
       (child.nodeType == nsIDOMNode.ELEMENT_NODE ||
        child.nodeType == nsIDOMNode.DOCUMENT_TYPE_NODE) &&
       (!replaced || child.nodeType != replaced.nodeType))
      for (var i = 0; i < parent.childNodes.length; i++)
        if (parent.childNodes[i].nodeType == child.nodeType)
          return false;
    return true;
  },
  
  getCommand: function(aCommand)
  {
  	if (aCommand in window)
	    return new window[aCommand]();
	  return null;
  },
  
  ////////////////////////////////////////////////////////////////////////////
  //// event dispatching

  addObserver: function(aEvent, aObserver) { this.mObsMan.addObserver(aEvent, aObserver); },
  removeObserver: function(aEvent, aObserver) { this.mObsMan.removeObserver(aEvent, aObserver); },
  
  ////////////////////////////////////////////////////////////////////////////
  //// UI Commands
  
  showFindDialog: function()
  {
    var win = openDialog("chrome://inspector/content/viewers/dom/findDialog.xul", 
                         "_blank", "chrome,dependent", this.mFindType, this.mFindDir, this.mFindParams);
  },

  /**
   * Toggles the setting for displaying anonymous content.
   */
  toggleAnonContent: function toggleAnonContent()
  {
    var value = PrefUtils.getPref("inspector.dom.showAnon");
    PrefUtils.setPref("inspector.dom.showAnon", !value);
  },

  /**
   * Sets the UI and filters for anonymous content.
   *
   * @param aValue The value that we should be setting things to.
   */
  setAnonContent: function setAnonContent(aValue)
  {
    this.mDOMView.showAnonymousContent = aValue;
    this.mPanel.panelset.setCommandAttribute("cmd:toggleAnon", "checked",
                                             aValue);
  },

  toggleSubDocs: function()
  {
    var val = !this.mDOMView.showSubDocuments;
    this.mDOMView.showSubDocuments = val;
    this.mPanel.panelset.setCommandAttribute("cmd:toggleSubDocs", "checked", val);
  },

  /**
   * Toggles the visibility of Processing Instructions.
   */
  toggleProcessingInstructions: function toggleProcessingInstructions()
  {
    var value = PrefUtils.getPref("inspector.dom.showProcessingInstructions");
    PrefUtils.setPref("inspector.dom.showProcessingInstructions", !value);
  },

  /**
   * Sets the visibility of Processing Instructions.
   *
   * @param aValue The visibility of the instructions.
   */
  setProcessingInstructions: function setProcessingInstructions(aValue)
  {
    this.mPanel.panelset.setCommandAttribute("cmd:toggleProcessingInstructions",
                                             "checked", aValue);
    if (aValue) {
      this.mDOMView.whatToShow |= NodeFilter.SHOW_PROCESSING_INSTRUCTION;
    } else {
      this.mDOMView.whatToShow &= ~NodeFilter.SHOW_PROCESSING_INSTRUCTION;
    }
  },

  /**
   * Toggle state of 'Show Accessible Nodes' option.
   */
  toggleAccessibleNodes: function toggleAccessibleNodes()
  {
    var value = PrefUtils.getPref("inspector.dom.showAccessibleNodes");
    PrefUtils.setPref("inspector.dom.showAccessibleNodes", !value);
  },

  /**
   * Set state of 'Show Accessible Nodes' option.
   *
   * @param Boolean aValue - if true then accessible nodes will be shown
   */
  setAccessibleNodes: function setAccessibleNodes(aValue)
  {
    if (!("@mozilla.org/accessibleRetrieval;1" in Components.classes))
      aValue = false;

    this.mDOMView.showAccessibleNodes = aValue;
    this.mPanel.panelset.setCommandAttribute("cmd:toggleAccessibleNodes",
                                             "checked", aValue);
    this.onItemSelected();
  },

  /**
   * Return state of 'Show Accessible Nodes' option.
   */
  getAccessibleNodes: function getAccessibleNodes()
  {
    return this.mDOMView.showAccessibleNodes;
  },

  /**
   * Toggles the value for whitespace nodes.
   */
  toggleWhitespaceNodes: function toggleWhitespaceNodes()
  {
    var value = PrefUtils.getPref("inspector.dom.showWhitespaceNodes");
    PrefUtils.setPref("inspector.dom.showWhitespaceNodes", !value);
  },

  /**
   * Sets the UI for displaying whitespace nodes.
   *
   * @param aValue The value we are to use to determine the state of the UI.
   */
  setWhitespaceNodes: function setWhitespaceNodes(aValue)
  {
    this.mPanel.panelset.setCommandAttribute("cmd:toggleWhitespaceNodes",
                                             "checked", aValue);
    this.mDOMView.showWhitespaceNodes = aValue;
  },

  showColumnsDialog: function()
  {
    var win = openDialog("chrome://inspector/content/viewers/dom/columnsDialog.xul", 
      "_blank", "chrome,dependent", this);
  },

  cmdShowPseudoClasses: function()
  {
    var idx = this.mDOMTree.currentIndex;
    var node = this.getNodeFromRowIndex(idx);
    
    if (node)
      openDialog("chrome://inspector/content/viewers/dom/pseudoClassDialog.xul", 
                           "_blank", "chrome", node);
  },

  cmdBlink: function()
  {
    this.flashElement(this.selectedNode);
  },
  
  cmdBlinkIsValid: function()
  {
    return this.selectedNode ? (this.selectedNode.nodeType == nsIDOMNode.ELEMENT_NODE) : false;
  },
    
  onItemSelected: function()
  {
    var idx = this.mDOMTree.currentIndex;
    this.mSelection = this.getNodeFromRowIndex(idx);
    this.mObsMan.dispatchEvent("selectionChange", { selection: this.mSelection } );
  
    if (this.mSelection) {
      if (this.mFlashSelected)
        this.flashElement(this.mSelection);
    }
    viewer.pane.panelset.updateAllCommands();
  },
  
  setInitialSelection: function(aObject)
  {
    var fireSelected = this.mDOMTree.currentIndex == 0;
  
    if (this.mPanel.params)
      this.selectElementInTree(this.mPanel.params);
    else
      this.selectElementInTree(aObject, true);
  
    if (fireSelected)
      this.onItemSelected();
  },
  
  onContextCreate: function(aPP)
  {
    var mi, cmd;
    for (var i = 0; i < aPP.childNodes.length; ++i) {
      mi = aPP.childNodes[i];
      if (mi.hasAttribute("observes")) {
        cmd = document.getElementById(mi.getAttribute("observes"));
        if (cmd && cmd.hasAttribute("isvalid")) {
          try {
            var isValid = new Function(cmd.getAttribute("isvalid"));
          } catch (ex) { /* die quietly on syntax error in handler */ }
          if (!isValid())
            mi.setAttribute("hidden", "true");
          else
            mi.removeAttribute("hidden");
        }
      }
    }
  },

  onCommandPopupShowing: function onCommandPopupShowing(aMenuPopup) {
    for (var i = 0; i < aMenuPopup.childNodes.length; i++) {
      var commandId = aMenuPopup.childNodes[i].getAttribute("command");
      if (viewer.isCommandEnabled(commandId))
        document.getElementById(commandId).setAttribute("disabled", "false");
      else
        document.getElementById(commandId).setAttribute("disabled", "true");
    }
  },
  
  cmdInspectBrowserIsValid: function()
  {
    var node = viewer.selectedNode;
    if (!node || node.nodeType != nsIDOMNode.ELEMENT_NODE) return false;
    
    var n = node.localName.toLowerCase();
    return n == "tabbrowser" || n == "browser" || n == "iframe" || n == "frame" || n == "editor";
  },
  
  cmdInspectBrowser: function()
  {
    var node = this.selectedNode;
    var n = node.localName.toLowerCase();
    if (node && n == "browser" && node.namespaceURI == kXULNSURI) {
      // xul browser
      this.subject = node.contentDocument;
    } else if (node && n == "tabbrowser" && node.namespaceURI == kXULNSURI) {
      // xul tabbrowser
      this.subject = node.contentDocument;
    } else if (n == "iframe" && node.namespaceURI == kXULNSURI) {
      // xul iframe
      this.subject = node.contentDocument;
    } else if (n == "iframe" || n == "frame") {
      // html iframe or frame
      this.subject = node.contentDocument;
    } else if (n == "editor") {
      // editor
      this.subject = node.contentDocument;
    }
  },
 
  cmdInspectInNewWindow: function()
  {
    var node = this.selectedNode;
    if (node)
      inspectObject(node);
  },
  
  ////////////////////////////////////////////////////////////////////////////
  //// XML Serialization

  cmdCopySelectedXML: function()
  {
    var node = this.selectedNode;
    if (node) {
      var xml = this.toXML(node);
    
      var helper = XPCU.getService(kClipboardHelperCID, "nsIClipboardHelper");
      helper.copyString(xml);
    }
  },

  toXML: function(aNode)
  {
    // we'll use XML serializer, if available
    if (typeof XMLSerializer != "undefined")
      return (new XMLSerializer()).serializeToString(aNode);
    else
      return this._toXML(aNode, 0);
  },
  
  // not the most complete serialization ever conceived, but it'll do for now
  _toXML: function(aNode, aLevel)
  {
    if (!aNode) return "";
    
    var s = "";
    var indent = "";
    for (var i = 0; i < aLevel; ++i)
      indent += "  ";
    var line = indent;        
      
    if (aNode.nodeType == nsIDOMNode.ELEMENT_NODE) {
      line += "<" + aNode.localName;

      var attrIndent = "";
      for (i = 0; i < line.length; ++i)
        attrIndent += " ";
  
      for (i = 0; i < aNode.attributes.length; ++i) {
        var a = aNode.attributes[i];
        var attr = " " + a.localName + '="' + InsUtil.unicodeToEntity(a.nodeValue) + '"';
        if (line.length + attr.length > 80) {
          s += line + (i < aNode.attributes.length-1 ? "\n"+attrIndent : "");
          line = "";
        }
        
        line += attr;
      }
      s += line;
      
      if (aNode.childNodes.length == 0)
        s += "/>\n";
      else {
        s += ">\n";
        for (i = 0; i < aNode.childNodes.length; ++i)
          s += this._toXML(aNode.childNodes[i], aLevel+1);
        s += indent + "</" + aNode.localName + ">\n";
      }
    } else if (aNode.nodeType == nsIDOMNode.TEXT_NODE) {
      s += InsUtil.unicodeToEntity(aNode.data);
    } else if (aNode.nodeType == nsIDOMNode.COMMENT_NODE) {
      s += line + "<!--" + InsUtil.unicodeToEntity(aNode.data) + "-->\n";
    } else if (aNode.nodeType == nsIDOMNode.DOCUMENT_NODE) {
      s += this._toXML(aNode.documentElement);
    }
    
    return s;
  },
  
  ////////////////////////////////////////////////////////////////////////////
  //// Click Selection

  selectByClick: function()
  {
    if (this.mSelecting) {
      this.stopSelectByClick();
      this.removeClickListeners();
    } else {
      // wait until after user releases the mouse after selecting this command from a UI element
      window.setTimeout("viewer.startSelectByClick()", 10);
    }
  },
  
  startSelectByClick: function()
  {
    this.mSelecting = true;
    this.mSelectDocs = this.getAllDocuments();

    for (var i = 0; i < this.mSelectDocs.length; ++i) {
      this.mSelectDocs[i].addEventListener("mousedown", MouseDownListener, true);
      this.mSelectDocs[i].addEventListener("mouseup", EventCanceller, true);
      this.mSelectDocs[i].addEventListener("click", ListenerRemover, true);
      // If use moves the mouse out of the original target area, there
      // will be no onclick event fired.... so we have to deal with
      // that.
      this.mSelectDocs[i].addEventListener("mouseout", ListenerRemover, true);
    }
    this.mPanel.panelset.setCommandAttribute("cmd:selectByClick", "checked", "true");
  },

  selectByClickOver: function(aTarget)
  {
    if (this.mLastOver)
      this.flasher.stop();

    this.flasher.element = aTarget;
    this.flasher.start(-1, 1, true);
    
    this.mLastOver = aTarget;
  },
  
  doSelectByClick: function(aTarget)
  {
    if (aTarget.nodeType == nsIDOMNode.TEXT_NODE)
      aTarget = aTarget.parentNode;
      
    this.stopSelectByClick();
    this.selectElementInTree(aTarget);
  },

  stopSelectByClick: function()
  {
    this.mSelecting = false;

    this.mPanel.panelset.setCommandAttribute("cmd:selectByClick", "checked", null);
  },

  removeClickListeners: function()
  {
    if (!this.mSelectDocs) // we didn't select an element by click
      return;

    for (var i = 0; i < this.mSelectDocs.length; ++i) {
      this.mSelectDocs[i].removeEventListener("mousedown", MouseDownListener, true);
      this.mSelectDocs[i].removeEventListener("mouseup", EventCanceller, true);
      this.mSelectDocs[i].removeEventListener("click", ListenerRemover, true);
      this.mSelectDocs[i].removeEventListener("mouseout", ListenerRemover, true);
    }
  },
  
  ////////////////////////////////////////////////////////////////////////////
  //// Find Methods

  startFind: function(aType, aDir)
  {
    this.mFindType = aType;
    this.mFindDir = aDir;
    this.mFindParams = [];
    for (var i = 2; i < arguments.length; ++i)
      this.mFindParams[i-2] = arguments[i];
      
    var fn = null;
    switch (aType) {
      case "id":
        fn = "doFindElementById";
        break;
      case "tag":
        fn = "doFindElementsByTagName";
        break;
      case "attr":
        fn = "doFindElementsByAttr";
        break;
    };

    this.mFindFn = fn;
    this.mFindWalker = this.createDOMWalker(this.mDOMView.rootNode);
    this.findNext();
  },
  
  findNext: function()
  {
    var walker = this.mFindWalker;
    var result = null;
    if (walker) {
      while (walker.currentNode) {
        //dump((walker.currentNode ? walker.currentNode.localName : "") + "\n");
        if (this[this.mFindFn](walker)) {
          result = walker.currentNode;
          walker.nextNode();
          break;
        }
        walker.nextNode();
      }
      
      if (result) {
        this.selectElementInTree(result);
        this.mDOMTree.focus();
      } else {
        var bundle = this.mPanel.panelset.stringBundle;
        var msg = bundle.getString("findNodesDocumentEnd.message");
        var title = bundle.getString("findNodesDocumentEnd.title");

        var promptService = XPCU.getService(kPromptServiceCID, "nsIPromptService");
        promptService.alert(window, title, msg);
      }
    }
  },

  doFindElementById: function(aWalker)
  {
    var re = new RegExp(this.mFindParams[0], "i");

    var node = aWalker.currentNode;
    if (!node)
      return false;

    if (node.nodeType != Components.interfaces.nsIDOMNode.ELEMENT_NODE)
      return false;

    for (var i = 0; i < node.attributes.length; i++) {
      var attr = node.attributes[i];
      if (attr.isId && re.test(attr.nodeValue)) {
        return true;
      }
    }

    return false;
  },

  doFindElementsByTagName: function(aWalker)
  {
    var re = new RegExp(this.mFindParams[0], "i");

    return aWalker.currentNode
           && aWalker.currentNode.nodeType == nsIDOMNode.ELEMENT_NODE
           && re.test(aWalker.currentNode.localName);
  },

  doFindElementsByAttr: function(aWalker)
  {
    var re = new RegExp(this.mFindParams[1], "i");

    return aWalker.currentNode
           && aWalker.currentNode.nodeType == nsIDOMNode.ELEMENT_NODE
           && (this.mFindParams[1] == ""
              ? aWalker.currentNode.hasAttribute(this.mFindParams[0])
              : re.test(aWalker.currentNode.getAttribute(this.mFindParams[0])));
  },
  
  ///////////////////////////////////////////////////////////////////////////
  // Takes an element from the document being inspected, finds the treeitem
  // which represents it in the DOM tree and selects that treeitem.
  //
  // @param aEl - element from the document being inspected
  ///////////////////////////////////////////////////////////////////////////

  selectElementInTree: function(aEl, aExpand, aQuickie)
  {
    var bx = this.mDOMTree.treeBoxObject;

    if (!aEl) {
      bx.view.selection.select(null);
      return false;      
    }
      
    // Keep searching until a pre-created ancestor is
    // found, and then open each ancestor until
    // the found element is created
    var walker = this.createDOMWalker(aEl);
    var line = [];
    var parent = aEl;
    var index = null;
    while (parent) {
      index = this.getRowIndexFromNode(parent);
      line.push(parent);
      if (index < 0) { 
        // row for this node hasn't been created yet
        parent = walker.parentNode();
      } else
        break;
    } 

    // we've got all the ancestors, now open them 
    // one-by-one from the top on down
    var view = bx.view;
    var lastIndex;
    for (var i = line.length-1; i >= 0; i--) {
      index = this.getRowIndexFromNode(line[i]);
      if (index < 0) 
        break; // can't find row, so stop trying to descend
      if ((aExpand || i > 0) && !view.isContainerOpen(index)) {
        view.toggleOpenState(index);
      }
      lastIndex = index;
    }

    if (!aQuickie && lastIndex >= 0) {
      view.selection.select(lastIndex);
      bx.ensureRowIsVisible(lastIndex);
    }
    
    return aQuickie;
  },
  
  ///////////////////////////////////////////////////////////////////////////
  // Remember which rows are open, and which row is selected. Then rebuild tree,
  // re-open previously opened rows, and re-select previously selected row
  ///////////////////////////////////////////////////////////////////////////
  rebuild: function()
  {
    var selNode = this.getNodeFromRowIndex(this.mDOMTree.currentIndex);
    this.mDOMTree.view.selection.select(null);
    
    var opened = [];
    var i;
    for (i = 0; i < this.mDOMView.rowCount; ++i) {
      if (this.mDOMView.isContainerOpen(i))
        opened.push(this.getNodeFromRowIndex(i));
    }
    
    this.mDOMView.rebuild();
    
    for (i = 0; i < opened.length; ++i)
      this.selectElementInTree(opened[i], true, true);
    
    this.selectElementInTree(selNode);
  },
  
  createDOMWalker: function(aRoot)
  {
    var walker = XPCU.createInstance("@mozilla.org/inspector/deep-tree-walker;1", "inIDeepTreeWalker");
    walker.showAnonymousContent = this.mDOMView.showAnonymousContent;
    walker.showSubDocuments = this.mDOMView.showSubDocuments;
    walker.init(aRoot, Components.interfaces.nsIDOMNodeFilter.SHOW_ALL);
    return walker;
  },
  
  ////////////////////////////////////////////////////////////////////////////
  //// Columns

  initColumns: function()
  {
    var colPref = PrefUtils.getPref("inspector.dom.columns");
    var cols = colPref.split(",")
    this.mColumns = cols;
    this.mColumnHash = {};
  },

  // XX re-implement custom columns code some-day  

  saveColumns: function()
  {
    var cols = this.mColumns.join(",");
    PrefUtils.setPref("inspector.dom.columns", cols);
  },

  ////////////////////////////////////////////////////////////////////////////
  //// Flashing

  get flasher()
  {
    if (!("mFlasher" in this)) {
      this.mFlasher = new Flasher(PrefUtils.getPref("inspector.blink.border-color"), 
                                  PrefUtils.getPref("inspector.blink.border-width"), 
                                  PrefUtils.getPref("inspector.blink.duration"), 
                                  PrefUtils.getPref("inspector.blink.speed"),
                                  PrefUtils.getPref("inspector.blink.invert"));
    }
    
    return this.mFlasher;
  },

  flashElement: function(aElement)
  {
    // make sure we only try to flash element nodes, and don't 
    // flash the documentElement (it's too darn big!)
    if (aElement.nodeType == nsIDOMNode.ELEMENT_NODE &&
        aElement != aElement.ownerDocument.documentElement) {
      var flasher = this.flasher;
      
      if (flasher.flashing) 
        flasher.stop();
        
      try {
        flasher.element = aElement;
        flasher.start();
      } catch (ex) {
      }
    }
  },

  /**
   * Toggles the preference for flashing selected elements.
   */
  toggleFlashSelected: function toggleFlashSelected()
  {
    var value = PrefUtils.getPref("inspector.blink.on");
    PrefUtils.setPref("inspector.blink.on", !value);
  },

  /**
   * Sets the object's value and the command for flashing selected objects.
   *
   * @param aValue The value to set it to.
   */
  setFlashSelected: function setFlashSelected(aValue)
  {
    this.mFlashSelected = aValue;
    this.mPanel.panelset.setCommandAttribute("cmd:flashSelected", "checked",
                                             aValue);
  },

  ////////////////////////////////////////////////////////////////////////////
  //// Prefs

  /**
   * Called by PrefChangeObserver.
   *
   * @param aName The name of the preference that has been changed.
   */
  onPrefChanged: function onPrefChanged(aName)
  {
    var value = PrefUtils.getPref(aName);

    if (aName == "inspector.dom.showAnon") {
      this.setAnonContent(value);
    } else if (aName == "inspector.dom.showProcessingInstructions") {
      this.setProcessingInstructions(value);
    } else if (aName == "inspector.dom.showAccessibleNodes") {
      this.setAccessibleNodes(value);
    } else if (aName == "inspector.dom.showWhitespaceNodes") {
      this.setWhitespaceNodes(value);
    } else if (aName == "inspector.blink.on") {
      this.setFlashSelected(value);

      // don't need to rebuild for this
      return;
    } else if (this.mFlasher) {
      if (aName == "inspector.blink.border-color") {
        this.mFlasher.color = value;
      } else if (aName == "inspector.blink.border-width") {
        this.mFlasher.thickness = value;
      } else if (aName == "inspector.blink.duration") {
        this.mFlasher.duration = value;
      } else if (aName == "inspector.blink.speed") {
        this.mFlasher.speed = value;
      } else if (aName == "inspector.blink.invert") {
        this.mFlasher.invert = value;
      }

      // don't need to rebuild for these
      return;
    }

    this.rebuild();
  },

  ////////////////////////////////////////////////////////////////////////////
  //// Uncategorized
  
  getAllDocuments: function()
  {
    var doc = this.mDOMView.rootNode;
    var results = [doc];
    this.findDocuments(doc, results);
    return results;
  },
  
  findDocuments: function(aDoc, aArray)
  {
    this.addKidsToArray(aDoc.getElementsByTagName("frame"), aArray);
    this.addKidsToArray(aDoc.getElementsByTagName("iframe"), aArray);
    this.addKidsToArray(aDoc.getElementsByTagNameNS(kXULNSURI, "browser"), aArray);
    this.addKidsToArray(aDoc.getElementsByTagNameNS(kXULNSURI, "tabbrowser"), aArray);
    this.addKidsToArray(aDoc.getElementsByTagNameNS(kXULNSURI, "editor"), aArray);
  },
  
  addKidsToArray: function(aKids, aArray)
  {
    for (var i = 0; i < aKids.length; ++i) {
      try {
        aArray.push(aKids[i].contentDocument);
        // Now recurse down into the kid and look for documents there
        this.findDocuments(aKids[i].contentDocument, aArray);
      } catch (ex) {
        // if we can't access the content document, skip it
      }
    }
  },
  
  get selectedNode()
  {
    var index = this.mDOMTree.currentIndex;
    return index >= 0 ? this.getNodeFromRowIndex(index) : null;
  },

  getNodeFromRowIndex: function(aIndex)
  {
    try {
      return this.mDOMView.getNodeFromRowIndex(aIndex);
    } catch (ex) {
      return null;
    }
  },
  
  getRowIndexFromNode: function(aNode)
  {
    try {
      return this.mDOMView.getRowIndexFromNode(aNode);
    } catch (ex) {
      return -1;
    }
  }
  
};

////////////////////////////////////////////////////////////////////////////
//// Command Objects

function cmdEditDelete() {}
cmdEditDelete.prototype =
{
  node: null,
  nextSibling: null,
  parentNode: null,
  
  // remove this line for bug 179621, Phase Three
  txnType: "standard",
  
  // required for nsITransaction
  QueryInterface: txnQueryInterface,
  merge: txnMerge,
  isTransient: false,
  redoTransaction: txnRedoTransaction,
  
  doTransaction: function()
  {
    var node = this.node ? this.node : viewer.selectedNode;
    if (node) {
      this.node = node;
      this.nextSibling = node.nextSibling;
      this.parentNode = node.parentNode;
      var selectNode = this.nextSibling;
      if (!selectNode) selectNode = node.previousSibling;
      if (!selectNode) selectNode = this.parentNode;
      viewer.selectElementInTree(selectNode);
      node.parentNode.removeChild(node);
    }
  },
  
  undoTransaction: function()
  {
    if (this.node)
      this.parentNode.insertBefore(this.node, this.nextSibling);
    viewer.selectElementInTree(this.node);
  }
};

function cmdEditCut() {}
cmdEditCut.prototype =
{
  cmdCopy: null,
  cmdDelete: null,
  
  // remove this line for bug 179621, Phase Three
  txnType: "standard",
  
  // required for nsITransaction
  QueryInterface: txnQueryInterface,
  merge: txnMerge,
  isTransient: false,
  redoTransaction: txnRedoTransaction,
  
  doTransaction: function() 
  {
    if (!this.cmdCopy) {
      this.cmdDelete = new cmdEditDelete();
      this.cmdCopy = new cmdEditCopy();
    }
    this.cmdCopy.doTransaction();
    this.cmdDelete.doTransaction();    
  },

  undoTransaction: function()
  {
    this.cmdDelete.undoTransaction();    
  }
};

function cmdEditCopy() {}
cmdEditCopy.prototype =
{
  copiedNode: null,
  
  // remove this line for bug 179621, Phase Three
  txnType: "standard",
  
  // required for nsITransaction
  QueryInterface: txnQueryInterface,
  merge: txnMerge,
  isTransient: true,
  redoTransaction: txnRedoTransaction,

  doTransaction: function()
  {
    var copiedNode = null;
    if (!this.copiedNode) {
      copiedNode = viewer.selectedNode.cloneNode(true);
      if (copiedNode) {
        this.copiedNode = copiedNode;
      }
    } else
      copiedNode = this.copiedNode;
      
    viewer.pane.panelset.setClipboardData(copiedNode, "inspector/dom-node", null);
  }
};

/**
 * Pastes the node on the clipboard as the next sibling of the selected node.
 */
function cmdEditPaste() {}
cmdEditPaste.prototype =
{
  pastedNode: null,
  pastedBefore: null,

  // remove this line for bug 179621, Phase Three
  txnType: "standard",
  
  // required for nsITransaction
  QueryInterface: txnQueryInterface,
  merge: txnMerge,
  isTransient: false,
  redoTransaction: txnRedoTransaction,
  
  doTransaction: function doTransaction()
  {
    var node = this.pastedNode ? this.pastedNode : viewer.pane.panelset.getClipboardData();
    var selected = this.pastedBefore ? this.pastedBefore : viewer.selectedNode;
    if (selected) {
      this.pastedNode = node.cloneNode(true);
      this.pastedBefore = selected;
      selected.parentNode.insertBefore(this.pastedNode, selected.nextSibling);
      return false;
    }
    return true;
  },
  
  undoTransaction: function undoTransaction()
  {
    if (this.pastedNode)
      this.pastedNode.parentNode.removeChild(this.pastedNode);
  }
};

/**
 * Pastes the node on the clipboard as the previous sibling of the selected 
 * node.
 */
function cmdEditPasteBefore() {}
cmdEditPasteBefore.prototype =
{
  pastedNode: null,
  pastedBefore: null,
  
  // remove this line for bug 179621, Phase Three
  txnType: "standard",
  
  // required for nsITransaction
  QueryInterface: txnQueryInterface,
  merge: txnMerge,
  isTransient: false,
  redoTransaction: txnRedoTransaction,
  
  doTransaction: function doTransaction()
  {
    var node = this.pastedNode ? this.pastedNode : viewer.pane.panelset.getClipboardData();
    var selected = this.pastedBefore ? this.pastedBefore : viewer.selectedNode;
    if (selected) {
      this.pastedNode = node.cloneNode(true);
      this.pastedBefore = selected;
      selected.parentNode.insertBefore(this.pastedNode, selected);
      return false;
    }
    return true;
  },
  
  undoTransaction: function undoTransaction()
  {
    if (this.pastedNode)
      this.pastedNode.parentNode.removeChild(this.pastedNode);
  }
};

/**
 * Pastes the node on the clipboard in the place of the selected node, 
 * overwriting it.
 */
function cmdEditPasteReplace() {}
cmdEditPasteReplace.prototype =
{
  pastedNode: null,
  originalNode: null,
  
  // remove this line for bug 179621, Phase Three
  txnType: "standard",
  
  // required for nsITransaction
  QueryInterface: txnQueryInterface,
  merge: txnMerge,
  isTransient: false,
  redoTransaction: txnRedoTransaction,
  
  doTransaction: function doTransaction()
  {
    var node = this.pastedNode ? this.pastedNode : viewer.pane.panelset.getClipboardData();
    var selected = this.originalNode ? this.originalNode : viewer.selectedNode;
    if (selected) {
      this.pastedNode = node.cloneNode(true);
      this.originalNode = selected;
      selected.parentNode.replaceChild(this.pastedNode, selected);
      return false;
    }
    return true;
  },
  
  undoTransaction: function undoTransaction()
  {
    if (this.pastedNode)
      this.pastedNode.parentNode.replaceChild(this.originalNode, this.pastedNode);
  }
};

/**
 * Pastes the node on the clipboard as the first child of the selected node.
 */
function cmdEditPasteFirstChild() {}
cmdEditPasteFirstChild.prototype =
{
  pastedNode: null,
  pastedBefore: null,
  
  // remove this line for bug 179621, Phase Three
  txnType: "standard",
  
  // required for nsITransaction
  QueryInterface: txnQueryInterface,
  merge: txnMerge,
  isTransient: false,
  redoTransaction: txnRedoTransaction,
  
  doTransaction: function doTransaction()
  {
    var node = this.pastedNode ? this.pastedNode : viewer.pane.panelset.getClipboardData();
    var selected = this.pastedBefore ? this.pastedBefore : viewer.selectedNode;
    if (selected) {
      this.pastedNode = node.cloneNode(true);
      this.pastedBefore = selected.firstChild;
      selected.insertBefore(this.pastedNode, this.pastedBefore);
      return false;
    }
    return true;
  },
  
  undoTransaction: function undoTransaction()
  {
    if (this.pastedNode)
      this.pastedNode.parentNode.removeChild(this.pastedNode);
  }
};
/**
 * Pastes the node on the clipboard as the last child of the selected node.
 */
function cmdEditPasteLastChild() {}
cmdEditPasteLastChild.prototype =
{
  pastedNode: null,
  selectedNode: null,
  
  // remove this line for bug 179621, Phase Three
  txnType: "standard",
  
  // required for nsITransaction
  QueryInterface: txnQueryInterface,
  merge: txnMerge,
  isTransient: false,
  redoTransaction: txnRedoTransaction,
  
  doTransaction: function doTransaction()
  {
    var node = this.pastedNode ? this.pastedNode : viewer.pane.panelset.getClipboardData();
    var selected = this.selectedNode ? this.selectedNode : viewer.selectedNode;
    if (selected) {
      this.pastedNode = node.cloneNode(true);
      this.selectedNode = selected;
      selected.appendChild(this.pastedNode);
      return false;
    }
    return true;
  },
  
  undoTransaction: function undoTransaction()
  {
    if (this.selectedNode)
      this.selectedNode.removeChild(this.pastedNode);
  }
};

/**
 * Pastes the node on the clipboard in the place of the selected node, making
 * the selected node its child.
 */
function cmdEditPasteAsParent() {}
cmdEditPasteAsParent.prototype =
{
  pastedNode: null,
  originalNode: null,
  originalParentNode: null,
  
  // remove this line for bug 179621, Phase Three
  txnType: "standard",
  
  // required for nsITransaction
  QueryInterface: txnQueryInterface,
  merge: txnMerge,
  isTransient: false,
  redoTransaction: txnRedoTransaction,
  
  doTransaction: function doTransaction()
  {
    var node = this.pastedNode ? this.pastedNode : viewer.pane.panelset.getClipboardData();
    var selected = this.originalNode ? this.originalNode : viewer.selectedNode;
    var parent = this.originalParentNode ? this.originalParentNode : selected.parentNode;
    if (selected) {
      this.pastedNode = node.cloneNode(true);
      this.originalNode = selected;
      this.originalParentNode = parent;
      parent.replaceChild(this.pastedNode, selected);
      this.pastedNode.appendChild(selected);
      return false;
    }
    return true;
  },
  
  undoTransaction: function undoTransaction()
  {
    if (this.pastedNode)
      this.originalParentNode.replaceChild(this.originalNode, this.pastedNode);
  }
};

/**
 * Generic prototype for inserting a new node somewhere
 */
function InsertNode() {}
InsertNode.prototype = 
{
  insertedNode: null,
  originalNode: null,
  attr: null,

  // remove this line for bug 179621, Phase Three
  txnType: "standard",

  // required for nsITransaction
  QueryInterface: txnQueryInterface,
  merge: txnMerge,
  isTransient: false,
  redoTransaction: txnRedoTransaction,

  insertNode: function insertNode()
  {
  },

  createNode: function createNode()
  {
    var doc = this.originalNode.ownerDocument;
    if (!this.attr) {
      this.attr = { type: null, value: null, namespaceURI: null, accepted: false,
                    enableNamespaces: doc.contentType != "text/html" };

      window.openDialog("chrome://inspector/content/viewers/dom/insertDialog.xul",
                        "insert", "chrome,modal,centerscreen", doc, this.attr);

    }

    if (this.attr.accepted) {
      switch (this.attr.type) {
        case nsIDOMNode.ELEMENT_NODE:
          if (this.attr.enableNamespaces)
            this.insertedNode = doc.createElementNS(this.attr.namespaceURI, this.attr.value);
          else
            this.insertedNode = doc.createElement(this.attr.value);
          break;
        case nsIDOMNode.TEXT_NODE:
          this.insertedNode = doc.createTextNode(this.attr.value);
          break;
      }
      return true;
    }
    return false;
  },

  doTransaction: function doTransaction()
  {
    var selected = this.originalNode ? this.originalNode : viewer.selectedNode;
    if (selected) {
      this.originalNode = selected;
      if (this.createNode()) {
        this.insertNode();
        return false;
      }
    }
    return true;
  },

  undoTransaction: function undoTransaction()
  {
    if (this.insertedNode)
      this.insertedNode.parentNode.removeChild(this.insertedNode);
  }
};

/**
 * Inserts a node after the selected node.
 */
function cmdEditInsertAfter() {}
cmdEditInsertAfter.prototype = new InsertNode();
cmdEditInsertAfter.prototype.insertNode = function insertNode() {
  this.originalNode.parentNode.insertBefore(this.insertedNode, this.originalNode.nextSibling);
};

/**
 * Inserts a node before the selected node.
 */
function cmdEditInsertBefore() {}
cmdEditInsertBefore.prototype = new InsertNode();
cmdEditInsertBefore.prototype.insertNode = function insertNode() {
  this.originalNode.parentNode.insertBefore(this.insertedNode, this.originalNode);
};

/**
 * Inserts a node as the first child of the selected node.
 */
function cmdEditInsertFirstChild() {}
cmdEditInsertFirstChild.prototype = new InsertNode();
cmdEditInsertFirstChild.prototype.insertNode = function insertNode() {
  this.originalNode.insertBefore(this.insertedNode, this.originalNode.firstChild);
};

/**
 * Inserts a node as the last child of the selected node.
 */
function cmdEditInsertLastChild() {}
cmdEditInsertLastChild.prototype = new InsertNode();
cmdEditInsertLastChild.prototype.insertNode = function insertNode() {
  this.originalNode.appendChild(this.insertedNode);
};


////////////////////////////////////////////////////////////////////////////
//// Listener Objects

var MouseDownListener = {
  handleEvent: function(aEvent)
  {
    var target = viewer.mDOMView.showAnonymousContent ? aEvent.originalTarget : aEvent.target;
    if (aEvent.type == "mousedown") {
      aEvent.stopPropagation();
      aEvent.preventDefault();
      viewer.doSelectByClick(target);
    }
    else if (aEvent.type == "mouseover")
      viewer.selectByClickOver(target);
  }
};

var EventCanceller = {
  handleEvent: function(aEvent)
  {
    aEvent.stopPropagation();
    aEvent.preventDefault();
  }
};

var ListenerRemover = {
  handleEvent: function(aEvent)
  {
    if (!viewer.mSelecting) {
      if (aEvent.type == "click") {
        aEvent.stopPropagation();
        aEvent.preventDefault();
      }
      viewer.removeClickListeners();
    }
  }
};

var PrefChangeObserver = {
  observe: function(aSubject, aTopic, aData)
  {
    viewer.onPrefChanged(aData);
  }
};

function gColumnAddListener(aIndex)
{
  viewer.onColumnAdd(aIndex);
}

function gColumnRemoveListener(aIndex)
{
  viewer.onColumnRemove(aIndex);
}

function dumpDOM2(aNode)
{
  dump(DOMViewer.prototype.toXML(aNode));
}
