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

/***************************************************************
* DOMNodeViewer --------------------------------------------
*  The default viewer for DOM Nodes
****************************************************************/

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

var viewer;

var gPromptService;

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

const kDOMViewCID          = "@mozilla.org/inspector/dom-view;1";

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

window.addEventListener("load", DOMNodeViewer_initialize, false);

function DOMNodeViewer_initialize()
{
  viewer = new DOMNodeViewer();
  viewer.initialize(parent.FrameExchange.receiveData(window));
}

////////////////////////////////////////////////////////////////////////////
//// class DOMNodeViewer 

function DOMNodeViewer()  // implements inIViewer
{
  this.mObsMan = new ObserverManager(this);

  this.mURL = window.location;
  this.mAttrTree = document.getElementById("olAttr");

  // prepare and attach the DOM DataSource
  this.mDOMView = XPCU.createInstance(kDOMViewCID, "inIDOMView");
  this.mDOMView.whatToShow = NodeFilter.SHOW_ATTRIBUTE;
  this.mAttrTree.treeBoxObject.view = this.mDOMView;
}

DOMNodeViewer.prototype = 
{
  ////////////////////////////////////////////////////////////////////////////
  //// Initialization
  
  mDOMView: null,
  mSubject: null,
  mPanel: null,

  get selectedIndex()
  {
    return this.mAttrTree.currentIndex;
  },

 /**
  * Returns an array of the selected indices
  * @return an array of indices
  */
  get selectedIndices()
  {
    var indices = [];
    var rangeCount = this.mAttrTree.view.selection.getRangeCount();
    for (var i = 0; i < rangeCount; ++i) {
      var start = {};
      var end = {};
      this.mAttrTree.view.selection.getRangeAt(i, start, end);
      for (var c = start.value; c <= end.value; ++c) {
        indices.push(c);
      }
    }
    return indices;
  },

 /**
  * Returns a DOMAttribute from the selected index
  * @return a DomAttribute
  */
  get selectedAttribute()
  {
    var index = this.selectedIndex;
    return index >= 0 ?
      new DOMAttribute(this.mDOMView.getNodeFromRowIndex(index)) : null;
  },

 /**
  * Returns an array of DOMAttributes from the selected indices
  * @return an array of DOMAttributes
  */
  get selectedAttributes()
  {
    var indices = this.selectedIndices;
    var attrs = [];
    for (var i = 0; i < indices.length; ++i) {
      attrs.push(new DOMAttribute(this.mDOMView.getNodeFromRowIndex(indices[i])));
    }
    return attrs;
  },

  ////////////////////////////////////////////////////////////////////////////
  //// interface inIViewer

  //// attributes 

  get uid() { return "domNode" },
  get pane() { return this.mPanel },

  get selection() { return null },

  get subject() { return this.mSubject },
  set subject(aObject) 
  {
    // the node value's textbox won't fire onchange when we change subjects, so 
    // let's fire it. this won't do anything if it wasn't actually changed
    viewer.pane.panelset.execCommand('cmdEditNodeValue');

    this.mSubject = aObject;
    var deck = document.getElementById("dkContent");

    switch (aObject.nodeType) {
      // things with useful nodeValues
      case Node.TEXT_NODE:
      case Node.CDATA_SECTION_NODE:
      case Node.COMMENT_NODE:
      case Node.PROCESSING_INSTRUCTION_NODE:
        deck.setAttribute("selectedIndex", 1);
        var txb = document.getElementById("txbTextNodeValue").value = 
                  aObject.nodeValue;
        break;
      //XXX this view is designed for elements, write a more useful one for
      // document nodes, etc.
      default:
        var bundle = this.pane.panelset.stringBundle;
        deck.setAttribute("selectedIndex", 0);
        
        this.setTextValue("localName", aObject.localName);
        this.setTextValue("nodeType", bundle.getString(aObject.nodeType));
        this.setTextValue("namespace", aObject.namespaceURI);

        if (aObject != this.mDOMView.rootNode) {
          this.mDOMView.rootNode = aObject;
          this.mAttrTree.view.selection.select(-1);
        }
    }
    
    this.mObsMan.dispatchEvent("subjectChange", { subject: aObject });
  },

  // methods

  initialize: function(aPane)
  {
    this.mPanel = aPane;
    aPane.notifyViewerReady(this);
  },

  destroy: function()
  {
    // the node value's textbox won't fire onchange when we change views, so 
    // let's fire it. this won't do anything if it wasn't actually changed
    viewer.pane.panelset.execCommand('cmdEditNodeValue');
  },

  isCommandEnabled: function(aCommand)
  {
    switch (aCommand) {
      case "cmdEditPaste":
        var flavor = this.mPanel.panelset.clipboardFlavor;
        return (flavor == "inspector/dom-attribute" ||
                flavor == "inspector/dom-attributes");
      case "cmdEditInsert":
        return true;
      case "cmdEditCut":
      case "cmdEditCopy":
      case "cmdEditDelete":
        return this.selectedAttribute != null;
      case "cmdEditEdit":
        return this.mAttrTree.currentIndex >= 0 &&
                 this.mAttrTree.view.selection.count == 1;
      case "cmdEditNodeValue":
        // this function can be fired before the subject is set
        if (this.subject) {
          // something with a useful nodeValue
          if (this.subject.nodeType == Node.TEXT_NODE ||
              this.subject.nodeType == Node.CDATA_SECTION_NODE ||
              this.subject.nodeType == Node.COMMENT_NODE ||
              this.subject.nodeType == Node.PROCESSING_INSTRUCTION_NODE) {
            // did something change?
            return this.subject.nodeValue != 
                   document.getElementById("txbTextNodeValue").value;
          }
        }
        return false;
    }
    return false;
  },
  
  getCommand: function(aCommand)
  {
    switch (aCommand) {
      case "cmdEditCut":
        return new cmdEditCut();
      case "cmdEditCopy":
        return new cmdEditCopy(this.selectedAttributes);
      case "cmdEditPaste":
        return new cmdEditPaste();
      case "cmdEditInsert":
        return new cmdEditInsert();
      case "cmdEditEdit":
        return new cmdEditEdit();
      case "cmdEditDelete":
        return new cmdEditDelete();
      case "cmdEditNodeValue":
        return new cmdEditNodeValue();
    }
    return null;
  },
  
  ////////////////////////////////////////////////////////////////////////////
  //// event dispatching

  addObserver: function(aEvent, aObserver) { this.mObsMan.addObserver(aEvent, aObserver); },
  removeObserver: function(aEvent, aObserver) { this.mObsMan.removeObserver(aEvent, aObserver); },

  ////////////////////////////////////////////////////////////////////////////
  //// Uncategorized

  setTextValue: function(aName, aText)
  {
    var field = document.getElementById("tx_"+aName);
    if (field)
      field.value = aText;
  }
};

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

function cmdEditCut() {}
cmdEditCut.prototype =
{
  cmdCopy: null,
  cmdDelete: null,
  doCommand: function()
  {
    if (!this.cmdCopy) {
      this.cmdDelete = new cmdEditDelete();
      this.cmdCopy = new cmdEditCopy(viewer.selectedAttributes);
    }
    this.cmdCopy.doTransaction();
    this.cmdDelete.doCommand();    
  },

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

function cmdEditPaste() {}
cmdEditPaste.prototype =
{
  pastedAttr: null,
  previousAttrValue: null,
  subject: null,
  flavor: null,
  
  doCommand: function()
  {
    var subject, pastedAttr, flavor;
    if (this.subject) {
      subject = this.subject;
      pastedAttr = this.pastedAttr;
      flavor = this.flavor;
    } else {
      subject = viewer.subject;
      pastedAttr = viewer.pane.panelset.getClipboardData();
      flavor = viewer.pane.panelset.clipboardFlavor;
      this.pastedAttr = pastedAttr;
      this.subject = subject;
      this.flavor = flavor;
      if (flavor == "inspector/dom-attributes") {
        this.previousAttrValue = [];
        for (var i = 0; i < pastedAttr.length; ++i) {
          this.previousAttrValue[pastedAttr[i].node.nodeName] =
            viewer.subject.getAttribute(pastedAttr[i].node.nodeName);
        }
      } else if (flavor == "inspector/dom-attribute") {
        this.previousAttrValue =
          viewer.subject.getAttribute(pastedAttr.node.nodeName);
      }
    }
    
    if (subject && pastedAttr) {
      if (flavor == "inspector/dom-attributes") {
        for (var i = 0; i < pastedAttr.length; ++i) {
          subject.setAttribute(pastedAttr[i].node.nodeName,
                               pastedAttr[i].node.nodeValue);
        }
      } else if (flavor == "inspector/dom-attribute") {
        subject.setAttribute(pastedAttr.node.nodeName,
                             pastedAttr.node.nodeValue);
      }
    }
  },
  
  undoCommand: function()
  {
    if (this.pastedAttr) {
      if (this.flavor == "inspector/dom-attributes") {
        for (var i = 0; i < this.pastedAttr.length; ++i) {
          if (this.previousAttrValue[this.pastedAttr[i].node.nodeName])
            this.subject.setAttribute(this.pastedAttr[i].node.nodeName,
                      this.previousAttrValue[this.pastedAttr[i].node.nodeName]);
          else
            this.subject.removeAttribute(this.pastedAttr[i].node.nodeName);
        }
      } else if (this.flavor == "inspector/dom-attribute") {
        if (this.previousAttrValue)
          this.subject.setAttribute(this.pastedAttr.node.nodeName,
                                    this.previousAttrValue);
        else
          this.subject.removeAttribute(this.pastedAttr.node.nodeName);
      }
    }
  }
};

function cmdEditInsert() {}
cmdEditInsert.prototype =
{
  attr: null,
  subject: null,
  
  promptFor: function()
  {
    var bundle = viewer.pane.panelset.stringBundle;
    var title = bundle.getString("newAttribute.title");
    var doc = viewer.subject.ownerDocument;
    var out = { name: null, value: null, namespaceURI: null, accepted: false };

    window.openDialog("chrome://inspector/content/viewers/domNode/domNodeDialog.xul",
                      "insert", "chrome,modal,centerscreen", out, title, doc);

    this.subject = viewer.subject;
    if (out.accepted)
      this.subject.setAttributeNS(out.namespaceURI, out.name, out.value);
    
    this.attr = this.subject.getAttributeNode(out.name);
    return false;
  },
  
  doCommand: function()
  {
    if (!this.attr)
      return this.promptFor();
    
    this.subject.setAttributeNS(this.attr.namespaceURI,
                                this.attr.nodeName,
                                this.attr.nodeValue);
    return false;
  },
  
  undoCommand: function()
  {
    if (this.attr && this.subject == viewer.subject)
      this.subject.removeAttributeNS(this.attr.namespaceURI,
                                     this.attr.localName);
  }
};

function cmdEditDelete() {}
cmdEditDelete.prototype =
{
  attrs: null,
  subject: null,
  
  doCommand: function()
  {
    var attrs = this.attrs ? this.attrs : viewer.selectedAttributes;
    if (attrs) {
      this.attrs = attrs;
      this.subject = viewer.subject;
      for (var i = 0; i < this.attrs.length; ++i) {
        this.subject.removeAttribute(this.attrs[i].node.nodeName);
      }
    }
  },
  
  undoCommand: function()
  {
    if (this.attrs) {
      for (var i = 0; i < this.attrs.length; ++i) {
        this.subject.setAttribute(this.attrs[i].node.nodeName,
                                  this.attrs[i].node.nodeValue);
      }
    }
  }
};

// XXX when editing the a attribute in this document:
// data:text/xml,<x a="hi&#x0a;lo&#x0d;go"/>
// You only get "hi" and not the mutltiline text (windows)
// This seems to work on Linux, but not very usable
function cmdEditEdit() {}
cmdEditEdit.prototype =
{
  attr: null,
  previousValue: null,
  newValue: null,
  previousNamespaceURI: null,
  newNamespaceURI: null,
  subject: null,
  
  promptFor: function()
  {
    var attr = viewer.selectedAttribute.node;
    if (attr) {
      var bundle = viewer.pane.panelset.stringBundle;
      var title = bundle.getString("editAttribute.title");
      var doc = attr.ownerDocument;
      var out = {
        name: attr.nodeName,
        value: attr.nodeValue,
        namespaceURI: attr.namespaceURI,
        accepted: false
      };

      window.openDialog("chrome://inspector/content/viewers/domNode/domNodeDialog.xul",
                        "edit", "chrome,modal,centerscreen", out, title, doc);

      if (out.accepted) {
        this.subject              = viewer.subject;
        this.newValue             = out.value;
        this.newNamespaceURI      = out.namespaceURI || null;
        this.previousValue        = attr.nodeValue;
        this.previousNamespaceURI = attr.namespaceURI;
        if (this.previousNamespaceURI == this.newNamespaceURI) {
          this.subject.setAttributeNS(this.previousNamespaceURI,
                                      attr.nodeName,
                                      out.value);
        } else {
          this.subject.removeAttributeNS(this.previousNamespaceURI,
                                         attr.localName);
          this.subject.setAttributeNS(out.namespaceURI,
                                      attr.nodeName,
                                      out.value);
        }
        this.attr = this.subject.getAttributeNode(attr.nodeName);
        return false;
      }
    }
    return true;
  },
  
  doCommand: function()
  {
    if (!this.attr)
      return this.promptFor();

    this.subject.removeAttributeNS(this.previousNamespaceURI,
                                   this.attr.localName);
    this.subject.setAttributeNS(this.newNamespaceURI,
                                this.attr.nodeName,
                                this.newValue);
    return false;
  },
  
  undoCommand: function()
  {
    if (this.attr) {
      if (this.previousNamespaceURI == this.newNamespaceURI) {
        this.subject.setAttributeNS(this.previousNamespaceURI,
                                    this.attr.nodeName,
                                    this.previousValue);
      } else {
        this.subject.removeAttributeNS(this.newNamespaceURI,
                                       this.attr.localName);
        this.subject.setAttributeNS(this.previousNamespaceURI,
                                    this.attr.nodeName,
                                    this.previousValue);
      }
    }
  }
};

/**
 * Handles editing of node values.
 */
function cmdEditNodeValue() {
  this.newValue = document.getElementById("txbTextNodeValue").value;
  this.subject = viewer.subject;
  this.previousValue = this.subject.nodeValue;
}
cmdEditNodeValue.prototype =
{
  // remove this line for bug 179621, Phase Three
  txnType: "standard",
  
  // required for nsITransaction
  QueryInterface: txnQueryInterface,
  merge: txnMerge,
  isTransient: false,

  doTransaction: function doTransaction()
  {
    this.subject.nodeValue = this.newValue;
  },
  
  undoTransaction: function undoTransaction()
  {
    this.subject.nodeValue = this.previousValue;
    this.refreshView();
  },

  redoTransaction: function redoTransaction()
  {
    this.doTransaction();
    this.refreshView();
  },

  refreshView: function refreshView() {
    // if we're still on the same subject, update the textbox
    if (viewer.subject == this.subject) {
      document.getElementById("txbTextNodeValue").value =
               this.subject.nodeValue;
    }
  }
};
