Components.utils.import("resource://app/modules/ecma-debug.jsm");

const serializer = new XMLSerializer();

const nsIDOMAttr              = Components.interfaces.nsIDOMAttr;
const nsIXPathGenerator       = Components.interfaces.nsIXPathGenerator;
const nsIDOMNodeFilter        = Components.interfaces.nsIDOMNodeFilter;
const xeIVerbosioUtilsService = Components.interfaces.xeIVerbosioUtilsService;

const xpathgen = Components.classes["@mozilla.org/xpath-generator;1"]
                           .createInstance(nsIXPathGenerator);

const verbosioUtilsClass = "@verbosio.mozdev.org/verbosio-utils;1";
const verbosioUtils = Components.classes[verbosioUtilsClass]
                                .getService(xeIVerbosioUtilsService);

var startNode = null;
var templateFrame = null;
var templateDoc = null;
var nodeSelector = null;
var workDeck = null;
var newAttrsCount = 0;

function setAttrXPath(aRow, aPanel) {
  // xpath for attribute
  var path = aPanel.getUserData("base-xpath") + "@";
  var attr = aRow.getUserData("owner-attr");
  if (attr.prefix) {
    path += attr.prefix + ":";
  }
  path += attr.localName;
  aRow.setUserData("output-xpath", path, null);
}

function initializeUIField(aRow) {
  var uiLabel = document.createElementNS(XUL_NS, "xul:description");
  var uiLabelValue;
  var rowType = aRow.getAttribute("type");
  switch (aRow.getAttribute("type")) {
    case "attr-row":
    case "text-row":
      var disabledField = aRow.getElementsByAttribute("rowid", "disabled")[0];
      if (disabledField) {
        disabledField.addEventListener("command", {
          handleEvent: function handleEvent(aEvent) {
            uiField.hidden = disabledField.checked;
            aRow.firstChild.disabled = disabledField.checked;
            if (aRow.firstChild.selected) {
              var control = aRow.firstChild.control;
              control.selectedIndex = control.itemCount - 1;
            }
          }
        }, false);
      }

      var uiLabelField = aRow.getElementsByAttribute("rowid", "ui-label")[0];
      uiLabelValue = uiLabelField.getAttribute("value");

      // Add an event listener to keep the labels in sync.
      uiLabelField.addEventListener("input", {
        handleEvent: function handleEvent(aEvent) {
          uiLabel.value = uiLabelField.value;
        }
      }, false);
      break;
  }
  uiLabel.setAttribute("value", uiLabelValue);

  var uiTypeField = aRow.getElementsByAttribute("rowid", "valueType")[0];
  var uiType = uiTypeField.getAttribute("value");
  var uiTypeNode = document.createElementNS(XUL_NS, uiType);

  // Set the markup:xpath attribute.
  var xpath = aRow.getUserData("output-xpath");
  assert(xpath, "No xpath?");
  assert(xpath.indexOf("/markup:output/") == -1, "Found 'markup:output'");
  uiTypeNode.setAttributeNS(MARKUP_NS, "markup:xpath", xpath);

  if (rowType == "attr-row") {
    var attrNameField = aRow.getElementsByAttribute("rowid", "attrName")[0];
    var origValue = attrNameField.getAttribute("value");
    assert(origValue, "No attribute name?");

    // Event listener for attribute name changes.
    attrNameField.addEventListener("input", {
      ownerPanel: aRow.getUserData("owner-panel"),
      handleEvent: function handleEvent(aEvent) {
        var attr = document.createAttributeNS(null, attrNameField.value);
        var oldAttr = aRow.getUserData("owner-attr");

        // Remove all traces of the original attribute from <markup:output/>.
        var outputNode = templateFrame.firstChild.getOutputNode(uiTypeNode);
        assert(outputNode, "No output node?");
        outputNode = outputNode.getAttributeNodeNS(oldAttr.namespaceURI,
                                                   oldAttr.localName);
        if (outputNode) {
          outputNode.ownerElement.removeAttributeNode(outputNode);
        }

        // Remove all traces of the original attribute from <markup:ui/>.
        this.ownerPanel.getUserData("current-ui-match").removeAttributeNS(
          oldAttr.namespaceURI,
          oldAttr.localName
        );

        // Set the new markup:xpath attribute.
        aRow.setUserData("owner-attr", attr, null);
        setAttrXPath(aRow, this.ownerPanel);
        var xpath = aRow.getUserData("output-xpath");
        uiTypeNode.setAttributeNS(MARKUP_NS, "markup:xpath", xpath);
      }
    }, false);
  }

  var uiField = document.createElementNS(XUL_NS, "xul:hbox");
  uiField.appendChild(uiLabel);
  uiField.appendChild(uiTypeNode);

  var uiSection = templateFrame.getUserData("ui-section");
  uiSection.firstChild.appendChild(uiField);

  aRow.setUserData("ui-field", uiField, null);
  aRow.setUserData("ui-input", uiTypeNode, null);

  uiTypeField.addEventListener("select", {
    handleEvent: function handleEvent(aEvent) {
      uiType = uiTypeField.getAttribute("value");
      var newUINode = document.createElementNS(XUL_NS, uiType);
      uiTypeNode.parentNode.replaceChild(newUINode, uiTypeNode);
      uiTypeNode.setUserData("owner-row", null, null);

      uiTypeNode = newUINode;
      aRow.setUserData("ui-input", uiTypeNode, null);
      var xpath = aRow.getUserData("output-xpath");
      uiTypeNode.setAttributeNS(MARKUP_NS, "markup:xpath", xpath);
    }
  }, false);
}

function initializeAttrRow(aRow, aAttr, aPanel) {
  aRow.setUserData("owner-attr", aAttr, null);
  aRow.setUserData("owner-panel", aPanel, null);

  var commentWalker = document.createTreeWalker(aRow,
                                                nsIDOMNodeFilter.SHOW_COMMENT,
                                                null,
                                                true);
  while (commentWalker.nextNode()) {
    var comment = commentWalker.currentNode;
    comment.parentNode.removeChild(comment);
    commentWalker.currentNode = aRow;
  }

  // attribute field label
  var uiLabelField = aRow.getElementsByAttribute("rowid", "ui-label")[0];
  uiLabelField.setAttribute("value", aAttr.nodeName);

  // attribute name: placeholder for namespace menulistset
  var attrNameCell = aRow.getElementsByAttribute("rowid", "attrName")[0];
  attrNameCell.setAttribute("value", aAttr.nodeName);

  setAttrXPath(aRow, aPanel);
  initializeUIField(aRow);
}

const inlineCtrl = {
  handleEvent: function handleEvent(aEvent) {
    var row = aEvent.target.selectedItem.parentNode;
    var rowType = row.getAttribute("type");
    assert(rowType, "No row type for an inline input???");
    var item = nodeSelector.view.getItemAtIndex(nodeSelector.currentIndex);

    var refNode = item.getUserData("referenceNode");

    var uiNode = refNode.getUserData("current-ui-match");
    var uiField = row.getUserData("ui-field");
    assert(uiNode, "No user-interface node?");
    if (rowType == "attr-row") {
      assert(uiField, "No user-interface field?");
    }

    var panel = aEvent.target.parentNode;

    var inlineUI = panel.getUserData("inline-ui-input");
    var oldUIField = panel.getUserData("inline-ui-field");
    if (inlineUI && oldUIField) {
      oldUIField.appendChild(inlineUI);
      oldUIField.removeAttribute("hidden");
    }

    assert(row.namespaceURI == XUL_NS, "Not a <xul:row/>?");
    assert(row.localName == "row", "Not a <xul:row/>?");
    inlineUI = row.getUserData("ui-input");

    if (rowType == "attr-row") {
      assert(inlineUI, "No inline UI?");
    }

    if (inlineUI && uiField && uiNode) {
      uiNode.appendChild(inlineUI);
      uiField.setAttribute("hidden", "true");
    }
    panel.setUserData("inline-ui-input", inlineUI, null);
    panel.setUserData("inline-ui-field", uiField, null);
  }
} // end inlineCtrl

function selectDeckPanel() {
  var item = nodeSelector.view.getItemAtIndex(nodeSelector.currentIndex);
  var panel = item.getUserData("workDeckPanel");
  if (!panel) {
    panel = workDeck.childNodes[1].cloneNode(true);

    // Reset the inline radio group.
    var radiogroup = panel.getElementsByTagNameNS(XUL_NS, "radiogroup")[0];
    var radios = panel.getElementsByTagNameNS(XUL_NS, "radio");
    var deckIndex = workDeck.childNodes.length;
    for (var i = 0; i < radios.length; i++) {
      var attrNode = radios[i].getAttributeNodeNS(null, "group");
      if (attrNode.nodeValue == radiogroup.id) {
        attrNode.nodeValue += "_" + deckIndex;
      }
    }
    radiogroup.id += "_" + deckIndex;

    // Set up the panel's base xpath to its user-interface node.
    var outputRef = item.getUserData("referenceNode");
    var uiRef = outputRef.getUserData("current-ui-match");
    assert(uiRef, "No ui reference?");

    var outputContainer = templateFrame.firstChild.getOutputContainer();
    dump("outputContainer: " + outputContainer.nodeName + "\n");
    dump("outputRef: " + outputRef.nodeName + "\n");

    if (getDebugPref()) {
      var parent = outputRef;
      do {
        if (parent == outputContainer) {
          break;
        }
        parent = parent.parentNode;
      } while (parent)
      assert(parent == outputContainer, "Where the hell is outputRef?");
    }

    var uiContainer = templateFrame.getElementsByTagNameNS(MARKUP_NS, "ui")[0];
    assert(uiContainer, "No user interface container?");
    var xpath = xpathgen.generateXPath(outputRef, outputContainer);
    xpath += "/";
    const badOutput = "../markup:output/";
    if (xpath.indexOf(badOutput) == 0) {
      warn(badOutput + " shouldn't be in the path");
      xpath = xpath.substr(badOutput.length);
    }
    dump('xpath: ' + xpath + "\n");

    panel.setUserData("base-xpath", xpath, null);

    // Set up the inline control switcher.
    radiogroup.addEventListener("select", inlineCtrl, false);
    panel.setUserData("inline-ui-field", null, null);

    // Set up the attribute rows.
    var baseRow = panel.getElementsByAttribute("type", "attr-row")[0];
    var refAttrs = item.getUserData("referenceNode").attributes;
    for (var i = 0; i < refAttrs.length; i++) {
      if (refAttrs[i].namespaceURI == XMLNS_NS) {
        // Skip it, we don't want XMLNS declarations on the template.
        continue;
      }
      var row = baseRow.cloneNode(true);
      initializeAttrRow(row, refAttrs[i], panel);

      baseRow.parentNode.insertBefore(row, baseRow);
    }
    baseRow.parentNode.removeChild(baseRow);
    panel.setUserData("baseAttrRow", baseRow, null);

    // Set up the text row.
    var textRow = panel.getElementsByAttribute("type", "text-row")[0];
    //var textField = textRow.getElementsByAttribute("rowid", "ui-contents")[0];
    //textRow.setUserData("ui-field", textField, null);
    textRow.setUserData("output-xpath", xpath + "text()", null);
    initializeUIField(textRow);

    item.setUserData("workDeckPanel", panel, null);
    workDeck.appendChild(panel);


    panel.setUserData("current-ui-match", uiRef, null);
    var hbox = document.createElementNS(XUL_NS, "xul:hbox");

    /* XXX ajvincent This is a hack to keep XUL elements properly sized inside
       a XUL vbox - but it will likely not fix the real problem if the uiRef is
       already a child of a horizontally-oriented box.
    */
    uiRef.parentNode.replaceChild(hbox, uiRef);
    hbox.appendChild(uiRef);
  }

  workDeck.selectedPanel = panel;
}

function toggleTextDisabled(aEvent) {
  var row = aEvent.target.parentNode;
}

function removeAttrRow(aEvent) {
  var row = aEvent.target.parentNode;
  row.parentNode.removeChild(row);

  var uiField = row.getUserData("ui-field");
  uiField.parentNode.removeChild(uiField);
}

function addAttrRow(aEvent) {
  var row = workDeck.selectedPanel.getUserData("baseAttrRow").cloneNode(true);
  assert(row.getAttribute("type") == "attr-row", "No attribute row type?")
  var attr = document.createAttributeNS(null, "a" + newAttrsCount++);
  row.setUserData("owner-attr", attr, null);
  var insertRow = aEvent.target.parentNode;
  dump(insertRow.nodeName + "\n");
  assert(insertRow.namespaceURI == XUL_NS, "Not a <xul:row/>?");
  assert(insertRow.localName == "row", "Not a <xul:row/>?");
  insertRow.parentNode.insertBefore(row, insertRow);

  initializeAttrRow(row, attr, workDeck.selectedPanel);
}

function initialize() {
  if (window.arguments[0] instanceof Components.interfaces.nsIDOMNode) {
    startNode = window.arguments[0].cloneNode(true);
  } else {
    var testSource = "<xul:button xmlns:xul='" + XUL_NS + "'/>";
    var parser = new DOMParser();
    var doc = parser.parseFromString(testSource, "application/xml");
    startNode = doc.firstChild;
  }

  // Initialize xpath generator's namespace map.
  templateDoc = verbosioUtils.getTemplateDocument(startNode.namespaceURI);
  var nsMap = {};
  var walker = templateDoc.createTreeWalker(templateDoc,
                                            nsIDOMNodeFilter.SHOW_ELEMENT,
                                            null,
                                            true);
  while (walker.nextNode()) {
    var attrs = walker.currentNode.attributes;
    for (var i = 0; i < attrs.length; i++) {
      if ((attrs[i].namespaceURI == XMLNS_NS) &&
          (attrs[i].localName != "xmlns") &&
          !(attrs[i].localName in nsMap)) {
        nsMap[attrs[i].localName] = attrs[i].nodeValue;
        xpathgen.addNamespace(attrs[i].nodeValue, attrs[i].localName);
      }

      else if (attrs[i].namespaceURI &&
               attrs[i].prefix &&
               !(attrs[i].prefix in nsMap)) {
        nsMap[attrs[i].prefix] = attrs[i].namespaceURI;
        xpathgen.addNamespace(attrs[i].namespaceURI, attrs[i].prefix);
      }
    }
  }
  walker = null;
  nsMap = null;

  nodeSelector  = document.getElementById("nodeSelector");
  templateFrame = document.getElementById("templateFrame");
  workDeck      = document.getElementById("workspace").firstChild;
  assert(workDeck.namespaceURI == XUL_NS, "Expected <xul:deck/>!");
  assert(workDeck.localName == "deck", "Expected <xul:deck/>!");

  // Set up the markup:template element.
  var templateNode = document.createElementNS(MARKUP_NS, "markup:template");

  // Set up the markup:output element.
  var outputNode   = document.createElementNS(MARKUP_NS, "markup:output");
  document.adoptNode(startNode);
  outputNode.appendChild(startNode);
  templateNode.appendChild(outputNode);

  // Set up the markup:ui element.
  var uiNode = document.createElementNS(MARKUP_NS, "markup:ui");
  var vbox   = document.createElementNS(XUL_NS, "xul:vbox");
  var startClone = startNode.cloneNode(true);
  vbox.appendChild(startClone);
  uiNode.appendChild(vbox);
  templateNode.appendChild(uiNode);

  /* Due to our having cloned startNode for the ui section, we need to find a
     path to the ui section descendant node for this panel.  This is necessary
     to support inline UI fields in the ui section (say, changing the label of
     a <xul:button/> inside the UI for the button.
  */
  var walker1 = document.createTreeWalker(startNode,
                                          nsIDOMNodeFilter.SHOW_ALL,
                                          null,
                                          true);
  var walker2 = document.createTreeWalker(startClone,
                                          nsIDOMNodeFilter.SHOW_ALL,
                                          null,
                                          true);
  startNode.setUserData("current-ui-match", startClone, null);
  while (walker1.nextNode()) {
    assert(walker2.nextNode(), "Out of synch already?");
    walker1.currentNode.setUserData("current-ui-match",
                                    walker2.currentNode,
                                    null);
  }
  assert(!walker2.nextNode(), "Out of synch?");
  walker1 = null;
  walker2 = null;

  /* From now on, changes to outputNode must be very carefully controlled,
     if allowed at all.  The user will be interacting with uiNode, and any
     changes to outputNode must be reflected in uiNode - though the converse is
     not true.
  */
  templateFrame.appendChild(templateNode);
  templateFrame.setUserData("ui-section", uiNode, null);

  // Set up the DOM tree view.
  domTreeViewMaker.refreshTree(nodeSelector, startNode);
  nodeSelector.currentIndex = 0;
  selectDeckPanel();
}
window.addEventListener("load", initialize, true);

function dumpSource() {
  var source = serializer.serializeToString(templateFrame, "application/xml");
  dump(source + "\n\n");
}
