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

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

/**
 * Get the inner document's matching node for a target node.
 *
 * @param aTargetNode A node in the original document.
 * @param aInnerDoc   The iframe's inner document.
 *
 * @return nsIDOMNode
 */
function getMatchingNode(aTargetNode, aInnerDoc) {
  const C_i = Components.interfaces;
  const UNORDERED_NODE = C_i.nsIDOMXPathResult.ANY_UNORDERED_NODE_TYPE;
  xpathgen.searchFlags = 0;

  if ((aTargetNode instanceof C_i.nsIDOMText) &&
      (aTargetNode.previousNode instanceof C_i.nsIDOMText)) {
    // We know the xpath generator reports a failure on this.
    return null;
  }

  var path = xpathgen.generateXPath(aTargetNode, aTargetNode.ownerDocument);
  assert(aInnerDoc instanceof C_i.nsIDOMXPathEvaluator, "Not a DOM document?");

  var expr = aInnerDoc.createExpression(path, xpathgen.resolver);
  var xpathResult = expr.evaluate(aInnerDoc, UNORDERED_NODE, null);
  assert(xpathResult instanceof C_i.nsIDOMXPathResult,
         "Not a XPath result?")
  dump("getMatchingNode passed\n");
  return xpathResult.singleNodeValue;
}

function isWhitespaceData(aNode) {
  return ((aNode instanceof Components.interfaces.nsIDOMText) &&
          !(/\S/.test(aNode.nodeValue)));
}

/**
 * Modify the selection or the document based on information from Verbosio.
 *
 * @param aType  String defining the event type.
 * @param aEvent nsIDOMEvent dispatched for master document (may be null)
 * @param aDescIndexes1 Descendant index array provided as a convenience.
 * @param aDescIndexes2 Descendant index array provided as a convenience.
 */
function notifyEvent(aType, aEvent, aDescIndexes1, aDescIndexes2) {
  const C_i = Components.interfaces;
  var i;
  var path = null;
  var langPack = null;
  var wsType = null;
  var textFound = null;
  var walker;
  const lpC = "@verbosio.mozdev.org/language-pack-service;1?namespaceURI=";

  switch (aType) {
    case "select":
      // fall through; we want to do the same as if we had a focus call.

    case "focus":
      this.sourceEditor.setAttribute("src", this.uri);
      break;

    case "blur":
      this.sourceEditor.setAttribute("src", "about:blank");
      floatBox.style.display = "none";
      floatBox.floatTarget = null;
      break;

    case "mutation":
      if (aEvent.type == "DOMSubtreeModified") {
        // just a bunch of noise for us now.
        return;
      }
      dump("\nreceived mutation event\n");
      dump("type: " + aEvent.type + "\n");

      var target;
      switch (aEvent.type) {
        case "DOMAttrModified":
        case "DOMNodeInserted":
          target = aEvent.relatedNode;
          break;

        default:
          target = aEvent.target;
      }

      if ((aEvent.type == "DOMNodeRemoved") ||
          (aEvent.type == "DOMNodeInserted")) {
        // We're going to need these.
        const filter = {
          namespaceURI: aEvent.relatedNode.namespaceURI,
          skipWS: true,
          acceptNode: function acceptNode(aNode) {
            // Exclude nodes under elements the language doesn't own.
            if (aNode.parentNode.namespaceURI != this.namespaceURI) {
              return C_i.nsIDOMNodeFilter.FILTER_SKIP;
            }

            return (isWhitespaceData(aNode) == this.skipWS) ?
                   C_i.nsIDOMNodeFilter.FILTER_SKIP :
                   C_i.nsIDOMNodeFilter.FILTER_ACCEPT;
          }
        }
        const filterArg = C_i.nsIDOMNodeFilter.SHOW_TEXT |
                          C_i.nsIDOMNodeFilter.SHOW_CDATA_SECTION;
        langPack = Components.classes[lpC + aEvent.relatedNode.namespaceURI]
                             .getService(C_i.xeIXMLLanguagePackService);
      }

      dump("target: " + target.nodeName + "\n");
      dump("new value: " + aEvent.newValue + "\n");

      xpathgen.searchFlags = 0;
      var innerDoc = this.sourceEditor.contentDocument;

      path = xpathgen.generateXPath(target, target.ownerDocument);
      dump("path: " + path + "\n");

      var innerNode = getMatchingNode(target, innerDoc);
      // Perhaps the inner document doesn't support whitespace nodes (XUL).
      if (!innerNode &&
          langPack &&
          isWhitespaceData(target)) {
        wsType = langPack.handleWhitespace(aEvent.relatedNode.localName);
        if (wsType & C_i.xeIXMLLanguagePackService.WHITESPACE_IGNORABLE) {
          // Nuts.  We have to manually check if there are whitespace kids.
          dump("doing manual check\n");

          walker = innerDoc.createTreeWalker(
                     innerDoc, filterArg, filter, null
                   );
          textFound = walker.nextNode();

          if (!textFound) {
            dump("whitespace not found\n");
            wsType |= C_i.xeIXMLLanguagePackService.WHITESPACE_REMOVE;
          } else {
            dump("textFound\n");
          }
        } // end ignorable whitespace check

        if (wsType & C_i.xeIXMLLanguagePackService.WHITESPACE_REMOVE) {
          // We need to strip whitespace data nodes from newNode.
          dump("stripping whitespace\n");
          filter.skipWS = false;
          walker = aEvent.target.ownerDocument.createTreeWalker(
                     aEvent.target, filterArg, filter, null
                   );

          textFound = null;
          if (isWhitespaceData(aEvent.target)) {
            dump("newNode is itself a whitespace data node.  Bail out.\n");
            return;
          }

          while (walker.nextNode()) {
            textFound = walker.currentNode;
            walker.currentNode = textFound.parentNode;
            walker.currentNode.removeChild(textFound);
            dump("removed whitespace node before insertion.\n");
          }
          assert(textFound, "No text nodes removed?");
          return;
        }
      } // end !innerNode && langPack && isWhitespaceData(target);

      assert(innerNode, "Expected a node from the iframe!");

      function dumpChildNames(aNodeList) {
        var arr = [];
        for (var i = 0; i < aNodeList.length; i++) {
          if (isWhitespaceData(aNodeList[i])) {
            continue;
          }
          arr.push(aNodeList[i].nodeName);
        }
        dump(arr.join() + "\n");
      }

      switch (aEvent.type) {
        case "DOMAttrModified":
        case "DOMCharacterDataModified":
          innerNode.nodeValue = aEvent.newValue;
          break;

        case "DOMNodeRemoved":
          innerNode.parentNode.removeChild(innerNode);
          break;

        case "DOMNodeInserted":
          // innerNode is the parent node
          var index = 0;
          var innerIndex = 0;
          var newNode = aEvent.target.cloneNode(true);

          langPack = Components.classes[lpC + aEvent.relatedNode.namespaceURI]
                               .getService(C_i.xeIXMLLanguagePackService);

          if (langPack) {
            wsType = langPack.handleWhitespace(aEvent.relatedNode.localName);
            if (wsType & C_i.xeIXMLLanguagePackService.WHITESPACE_IGNORABLE) {
              // Nuts.  We have to manually check if there are whitespace kids.
              dump("doing manual check\n");

              var walker = innerDoc.createTreeWalker(
                             innerDoc, filterArg, filter, null
                           );
              var textFound = walker.nextNode();

              if (!textFound) {
                dump("whitespace not found\n");
                wsType |= C_i.xeIXMLLanguagePackService.WHITESPACE_REMOVE;
              } else {
                dump("textFound\n");
              }
            } // end ignorable whitespace check

            if (wsType & C_i.xeIXMLLanguagePackService.WHITESPACE_REMOVE) {
              // We need to strip whitespace data nodes from newNode.
              dump("stripping whitespace\n");
              filter.skipWS = false;
              walker = aEvent.target.ownerDocument.createTreeWalker(
                         newNode, filterArg, filter, null
                       );

              textFound = null;
              if (isWhitespaceData(newNode)) {
                dump("newNode is itself a whitespace data node.  Bail out.\n");
                return;
              }

              while (walker.nextNode()) {
                textFound = walker.currentNode;
                walker.currentNode = textFound.parentNode;
                walker.currentNode.removeChild(textFound);
                dump("removed whitespace node before insertion.\n");
              }
            }
          } // end if (langPack)

          while (aEvent.relatedNode.childNodes[index] != aEvent.target) {
            // See above block comment about whitespace data nodes.
            var relChild = aEvent.relatedNode.childNodes[index];
            var innerChild = innerNode.childNodes[innerIndex];
            if (relChild.nodeType == innerChild.nodeType) {
              innerIndex++;
            }

            index++;
          }

          innerDoc.adoptNode(newNode);
          innerNode.insertBefore(newNode, innerNode.childNodes[innerIndex]);
          dumpChildNames(aEvent.relatedNode.childNodes);
          dumpChildNames(innerNode.childNodes);
          break;

        default:
          warn("Unsupported event type!");
      }

      break;

    case "repaint":
      break;

    default:
      throw new Error("notifyEvent called with invalid value!");
  }
}

var floatBox;

/**
 * Get a viewer node for a given document.
 *
 * @param aDocumentWrapper xeIDocumentWrapper needing a viewer.
 *
 * @return clone of inspectorView element.
 */
function getViewerNode(aDocumentWrapper) {
  const xeIXMLDocumentWrapper = Components.interfaces.xeIXMLDocumentWrapper;
  if (aDocumentWrapper instanceof xeIXMLDocumentWrapper) {
    var hboxView = document.createElementNS(XUL_NS, "xul:hbox");
    var viewer = document.createElementNS(XUL_NS, "xul:iframe");
    viewer.setAttribute("type", "content");
    viewer.setAttribute("style",
                        "width: 600px; height: 400px;");

    floatBox = document.getElementByAttrKey(uuid, "floatbox");
    floatBox.floatTarget = null;
    assert(floatBox, "No float box?");

    var doc = aDocumentWrapper.wrappedDocument;

    viewer.addEventListener("error", function silence(evt) {
      dump("onerror called\n");
      evt.preventDefault();
    }, true, true);
    viewer.addEventListener("click", function selectNode(evt) {
      if (evt.target == floatBox.floatTarget) {
        return;
      }

      var xpath = xpathgen.generateXPath(evt.target, evt.target.ownerDocument);
      const nsIDOMXPathResult = Components.interfaces.nsIDOMXPathResult;
      const UNORDERED_NODE = nsIDOMXPathResult.ANY_UNORDERED_NODE_TYPE;
      var xpathExpression = doc.createExpression(xpath, xpathgen.resolver);
      var xpathResult = xpathExpression.evaluate(doc,
                                                 UNORDERED_NODE,
                                                 null);
      var origNode = xpathResult.singleNodeValue;
      if (!origNode) {
        floatBox.style.display = "none";
        floatBox.floatTarget = null;
        return;
      }

      var templateCount = {};
      var templateSet = verbosio.utils.getSupportingTemplates(
        origNode,
        templateCount
      );

      if (templateCount.value == 0) {
        floatBox.style.display = "none";
        floatBox.floatTarget = null;
        return;
      }
      floatBox.style.display = "-moz-box";
      floatBox.firstChild.templateSet = templateSet;
      floatBox.firstChild.origNode    = origNode;

      floatBox.style.position = "fixed";
      var leftCoord = (evt.clientX + viewer.boxObject.screenX);
      var topCoord = (evt.clientY + viewer.boxObject.screenY);
      floatBox.style.left = leftCoord + "px";
      floatBox.style.top = topCoord + "px";
      assert(leftCoord < window.innerWidth, "Too far to the right!");
      assert(topCoord < window.innerHeight, "Too far down the screen!");
      floatBox.floatTarget = evt.target;
    }, true, true);

    hboxView.appendChild(viewer);

    hboxView.setAttributeNS(null, "scopeid", uuid);
    hboxView.sourceEditor = viewer;
    viewer.setUserData("wrapper", aDocumentWrapper, null);
    hboxView.notifyEvent = notifyEvent;
    hboxView.initializeView = function initializeView() {
      const vFS = verbosio.virtualFileService;
      vFS.setDocumentPack(gDocumentPack);
      dump("public URI: " + aDocumentWrapper.publicURI + "\n");
      this.uri = vFS.convertURIToVirtual(aDocumentWrapper.publicURI);
    };
    return hboxView;
  }
  return null; // not yet implemented
}

// XXX ajvincent Localize!
window.addEventListener("load", function addPreviewTab(evt) {
  documentViewer.appendViewerTab("Preview", uuid, "p");
}, true);
