/* ***** 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 XPath Generator.
 *
 * 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 SUPPORTED_FLAGS = 0x00000003;

const C_i = Components.interfaces;

const ERROR_FAILURE     = Components.results.NS_ERROR_FAILURE;
const ERROR_UNEXPECTED  = Components.results.NS_ERROR_UNEXPECTED;
const INVALID_ARG       = Components.results.NS_ERROR_INVALID_ARG;
const NULL_POINTER      = Components.results.NS_ERROR_NULL_POINTER;
const NO_INTERFACE      = Components.results.NS_ERROR_NO_INTERFACE;
const DOM_NOT_SUPPORTED = (1 << 31 | (14+0x45) << 16 | 9);
//const DOM_NOT_SUPPORTED = Components.results.NS_ERROR_DOM_NOT_SUPPORTED_ERR;

/**
 * Escape the quotes in a string for XPath.
 *
 * @param aString Original string.
 *
 * @return Escaped string.
 */
function EscapeQuotes(aString) {
  if (aString.indexOf("'") == -1) {
    return "'" + aString + "'";
  }

  var quoteIndex = aString.indexOf('"');
  if (quoteIndex == -1) {
    return '"' + aString + '"';
  }

  var retval = 'concat("';
  var str = aString;

  var previousQuoteIndex = 0;
  while (quoteIndex != -1) {
    retval += str.substring(previousQuoteIndex, quoteIndex);
    retval += "\", '\"', \"";
    previousQuoteIndex = quoteIndex + 1;
    quoteIndex = str.indexOf('"');
  }

  retval += str + '")';
  return retval;
}

function XPathResolver() {
  this.mNamespaceURIMap = {};
  this.mPrefixMap = {};
  this.mPrefixesUndeclared = 0;
}
XPathResolver.prototype = {
  /**
   * Add a namespace URI and prefix.
   *
   * @param namespaceURI     The namespace URI of the namespace.
   * @param prefix           The prefix of the namespace.
   */
  addNamespace: function addNamespace(namespaceURI, prefix) {
    if (!namespaceURI || !prefix) {
      throw INVALID_ARG;
    }

    // Can we validate the prefix somehow via C_i.nsIParserService?

    if (prefix in this.mNamespaceURIMap) {
      // The prefix has already been assigned a namespace.
      dump("prefix already assigned\n");
      throw ERROR_FAILURE;
    }

    this.mPrefixMap[namespaceURI] = prefix;
    this.mNamespaceURIMap[prefix] = namespaceURI;
  },

  /**
   * Get or create a prefix corresponding to a particular namespace URI.
   *
   * @param aNamespaceURI    The namespace URI of the namespace.
   * @return prefix          The prefix of the namespace.
   */
  lookupPrefix: function lookupPrefix(aNamespaceURI) {
    if (!aNamespaceURI) {
      throw INVALID_ARG;
    }

    if (aNamespaceURI in this.mPrefixMap) {
      return this.mPrefixMap[aNamespaceURI];
    }

    var prefix = "a" + this.mPrefixesUndeclared;
    while (prefix in this.mNamespaceURIMap) {
      this.mPrefixesUndeclared++;
      prefix = "a" + this.mPrefixesUndeclared;
    }

    this.addNamespace(aNamespaceURI, prefix);
    return prefix;
  },

  // nsIDOMXPathNSResolver
  lookupNamespaceURI: function lookupNamespaceURI(aPrefix) {
    if (!aPrefix) {
      throw INVALID_ARG;
    }
    // Can we validate the prefix somehow via nsIParserService?

    if (aPrefix in this.mNamespaceURIMap) {
      return this.mNamespaceURIMap[aPrefix];
    }
    return null;
  },

  // nsIClassInfo
  getInterfaces: function getInterfaces(aCount) {
    var array = [C_i.nsIDOMXPathNSResolver,
                 C_i.nsIClassInfo];
    aCount.value = array.length;
    return array;
  },

  // nsIClassInfo
  getHelperForLanguage: function getHelperForLanguage(aLanguage) {
    return null;
  },

  // nsIClassInfo
  contractID: "@mozilla.org/xpath-generator-resolver;1",

  // nsIClassInfo
  classDescription: "XPathGeneratorResolver",

  // nsIClassInfo
  classID: Components.ID("{e74ada91-4c42-4493-b6e0-fa0fe02449e0}"),

  // nsIClassInfo
  implementationLanguage: C_i.nsIProgrammingLanguage.JAVASCRIPT,

  // nsIClassInfo
  flags: C_i.nsIClassInfo.DOM_OBJECT,

  // nsISupports
  QueryInterface: function QueryInterface(aIID) {
    if (aIID.equals(C_i.nsIDOMXPathNSResolver) ||
        aIID.equals(C_i.nsIClassInfo) ||
        aIID.equals(C_i.nsISupports))
      return this;

    throw NO_INTERFACE;
  }
};

function XPathGenerator() {
  // nsIXPathGenerator
  this.resolver = new XPathResolver();

  /* XXX ajvincent Bug 360207 prevents us from doing any sanity checks
   * on users setting searchFlags (getters/setters don't work right now
   * with CAPS).  We should be able to validate what users set it to.
   */
  // nsIXPathGenerator
  this.searchFlags = 0;
}
XPathGenerator.prototype = {
  /**
   * Get a XPath step, without index, based on the current iterator node.
   *
   * @param aNode Node to generate a step from.
   *
   * @return step  XPath step retrieved.
   * @return idval id attribute value.
   */
  getStepWithoutIndex: function getStepWithoutIndex(aNode) {
    if (!(aNode instanceof C_i.nsIDOMNode)) {
      throw ERROR_UNEXPECTED;
    }

    var step = "";
    var idval = null;

    switch (aNode.nodeType) {
      case C_i.nsIDOMNode.ELEMENT_NODE:
        if (!(aNode instanceof C_i.nsIDOMElement)) {
          throw ERROR_UNEXPECTED;
        }
        if (!(this.searchFlags & C_i.nsIXPathGenerator.IGNORE_ID_TYPE_ATTRS)) {
          for (var i = 0; i < aNode.attributes.length; i++) {
            var attrNode = aNode.attributes.item(i);
            if (attrNode.isId) {
              idval = attrNode.nodeValue;
              step = "id(" + EscapeQuotes(attrNode.nodeValue) + ")";
              return [step, idval];
            }
          }
        }

        var prefix = "";
        if (aNode.namespaceURI) {
          prefix = this.resolver.lookupPrefix(aNode.namespaceURI);
        }

        step = prefix ? prefix + ":" : "";
        step += aNode.localName;
        break;

      case C_i.nsIDOMNode.TEXT_NODE:
      case C_i.nsIDOMNode.CDATA_SECTION_NODE:
        step = "text()";
        break;

      case C_i.nsIDOMNode.PROCESSING_INSTRUCTION_NODE:
        if (!(aNode instanceof C_i.nsIDOMProcessingInstruction)) {
          throw ERROR_UNEXPECTED;
        }
        step = "processing-instruction('" + aNode.target + "')";
        break;

      case C_i.nsIDOMNode.COMMENT_NODE:
        step = "comment()";
        break;

      case C_i.nsIDOMNode.DOCUMENT_NODE:
      case C_i.nsIDOMNode.DOCUMENT_FRAGMENT_NODE:
        step = "/";
        break;

      default:
        throw ERROR_UNEXPECTED;
    }

    return [step, idval];
  },

  /**
   * Compare two nodes to see if a step might match them.
   *
   * @param aTargetIterator First node.
   * @param aComparison     Second node.
   * @param idval           id attribute value.
   *
   * @return nsIDOMNodeFilter.FILTER_ACCEPT if the step would match both.
   * @return nsIDOMNodeFilter.FILTER_SKIP   otherwise.
   */
  compareNodes: function compareNodes(aTargetIterator, aComparison, idval) {
    if (aComparison == aTargetIterator) {
      return C_i.nsIDOMNodeFilter.FILTER_ACCEPT;
    }

    if ((aTargetIterator instanceof C_i.nsIDOMText) &&
        (aComparison instanceof C_i.nsIDOMText)) {
      return C_i.nsIDOMNodeFilter.FILTER_ACCEPT;
    }

    if (aComparison.nodeType != aTargetIterator.nodeType) {
      return C_i.nsIDOMNodeFilter.FILTER_SKIP;
    }

    if (idval && aComparison.nodeType == C_i.nsIDOMNode.ELEMENT_NODE) {
      for (var i = 0; i < aComparison.attributes.length; i++) {
        var attrNode = aComparison.attributes.item(i);
        if (attrNode.isId) {
          // Stupid page authors.  We'll be nice to them.
          return attrNode.nodeValue == idval ?
                 C_i.nsIDOMNodeFilter.FILTER_ACCEPT :
                 C_i.nsIDOMNodeFilter.FILTER_SKIP;
        }
      }
    }

    if ((aComparison.localName != aTargetIterator.localName) ||
        (aComparison.namespaceURI != aTargetIterator.namespaceURI)) {
      return C_i.nsIDOMNodeFilter.FILTER_SKIP;
    }

    return C_i.nsIDOMNodeFilter.FILTER_ACCEPT;
  },

  // nsIXPathGenerator
  addNamespace: function addNamespace(namespaceURI, prefix) {
    this.resolver.addNamespace(namespaceURI, prefix);
  },

  // nsIXPathGenerator
  generateXPath: function generateXPath(targetNode, contextNode) {
    if (!targetNode || !contextNode) {
      throw NULL_POINTER;
    }

    // XPath doesn't support doctypes, so neither do we.
    if ((targetNode.nodeType == C_i.nsIDOMNode.DOCUMENT_TYPE_NODE) ||
        (contextNode.nodeType == C_i.nsIDOMNode.DOCUMENT_TYPE_NODE)) {
      throw DOM_NOT_SUPPORTED;
    }

    if (targetNode == contextNode) {
      return ".";
    }

    // Start establishing the common ancestor.
    var fromContextToAncestor = "";
    var contextIterator = contextNode;
    if (contextNode.nodeType == C_i.nsIDOMNode.ATTRIBUTE_NODE) {
      fromContextToAncestor = "..";
      if (!(contextNode instanceof C_i.nsIDOMAttr)) {
        throw ERROR_UNEXPECTED;
      }
      contextIterator = contextNode.ownerElement;
    }

    var targetIterator = targetNode;
    if (targetNode.nodeType == C_i.nsIDOMNode.ATTRIBUTE_NODE) {
      if (!(targetNode instanceof C_i.nsIDOMAttr)) {
        throw ERROR_UNEXPECTED;
      }
      targetIterator = targetNode.ownerElement;
    }

    // Unowned attributes will force an abort here.
    if (!targetIterator || !contextIterator) {
      dump("unowned attribute?  missing an iterator\n");
      throw ERROR_FAILURE;
    }

    var doc = (targetNode.nodeType == C_i.nsIDOMNode.DOCUMENT_NODE) ?
               targetNode :
               targetNode.ownerDocument;

    // Get the ancestor node.
    // Are the target and context nodes in the same document?
    if (!(doc instanceof C_i.nsIDOMDocumentRange) ||
        !(targetIterator instanceof C_i.nsIDOM3Node)) {
      throw ERROR_UNEXPECTED;
    }
    var range = doc.createRange();
    try {
      var compare = targetIterator.compareDocumentPosition(contextIterator);
      if (compare & C_i.nsIDOM3Node.DOCUMENT_POSITION_DISCONNECTED) {
        dump("disconnected\n");
        throw ERROR_FAILURE;
      }
      else if (compare & C_i.nsIDOM3Node.DOCUMENT_POSITION_PRECEDING) {
        range.setEndAfter(targetIterator);
        if (contextIterator.parentNode) {
          range.setStartBefore(contextIterator)
        } else {
          range.setStart(contextIterator, 0);
        }
      } else {
        range.setEndAfter(contextIterator);
        if (targetIterator.parentNode) {
          range.setStartBefore(targetIterator)
        } else {
          range.setStart(targetIterator, 0);
        }
      }
    } catch (e) {
      /* The whole point of the range is to determine the common ancestor.
       * Exceptions will be triggered when there isn't one.  But all the
       * same, we don't want to feed the user a range error when it's ours.
       */

      dump("range boundary failure\n");
      throw ERROR_FAILURE;
    }

    var ancestor = range.commonAncestorContainer;
    range.detach();
    range = null;

    /* We now have the ancestor node.  Start generating steps from the target
     * node to the ancestor, in reverse order.  (We skip the path from context
     * to ancestor for now, as that path may be unnecessary.)
     */
    var fromAncestorToTarget = "";
    var prefix;

    if (targetNode.nodeType == C_i.nsIDOMNode.ATTRIBUTE_NODE) {
      // Attributes don't fit cleanly in tree walkers or child nodes.
      prefix = "";
      if (targetNode.namespaceURI) {
        prefix = this.resolver.lookupPrefix(targetNode.namespaceURI);
      }
      fromAncestorToTarget = "@" + (prefix ? prefix + ":" : "");
      fromAncestorToTarget += targetNode.localName;
    }

    // Initialize for walking the tree.
    var step;
    idval = null;
    var total;
    var index;

    var checkPreviousNode = false;
    var prevNode = null;

    if (!(this.searchFlags &
          C_i.nsIXPathGenerator.USE_DESCENDANT_AXIS)) {
      // The user wants a path like "xul:box[2]/xul:textbox/@value".
      while (targetIterator != ancestor) {
        checkPreviousNode = (targetIterator instanceof C_i.nsIDOMText);
        [step, idval] = this.getStepWithoutIndex(targetIterator);

        /* We now have the step, but we may need to find the subscript index.
           The target iterator's index represents the ordinal number in brackets
           at the end of a step.  We will only include the ordinal number if it
           is necessary (if the total number of nodes matching this step is
           greater than one).

           However, we do not support adjacent DOM data nodes when we're trying
           to generate a path to the second DOM node. XPath treats CDATA and
           text nodes as the same type of node, so if a text node follows a
           CDATA node immediately (with no element in between, for example), we
           must report a failure.

           If the child node list is [text, cdata, comment, text], we cannot
           match the cdata node.  If we're trying to match either
           of the text nodes, we will pass.
         */
        var indexIterator = targetIterator.parentNode.firstChild;
        prevNode = null; // There's nothing before the index iterator.
        total = 0;
        index = 0;

        while (indexIterator) {
          var filterValue = this.compareNodes(targetIterator,
                                              indexIterator,
                                              idval);
          if (filterValue == C_i.nsIDOMNodeFilter.FILTER_ACCEPT) {
            total++;

            /* Conditions:
               checkPreviousNode: targetIterator is a data node.
               !index: We haven't determined an index yet.
               indexIterator == targetIterator: We're at the node we would
                 set an index for.
               prevNode instanceof nsIDOMText: We're not at the first child
                 node, and prevNode is a data node.

               If all these conditions are met, we can't absolutely guarantee
               the XPath we generate is correct.
             */
            if (checkPreviousNode && !index &&
                (indexIterator == targetIterator) &&
                (prevNode instanceof C_i.nsIDOMText)) {
              // We don't support two adjacent character data nodes.
              dump("adjacent data nodes!!!\n");
              throw ERROR_FAILURE;
            }
          }

          if (indexIterator == targetIterator) {
            index = total;
          }

          prevNode      = indexIterator;
          indexIterator = indexIterator.nextSibling;
        }

        if (total > 1) {
          step += "[" + index + "]";
        }

        // Add the step.
        fromAncestorToTarget = step + "/" + fromAncestorToTarget;
        if (idval) {
          break;
        }

        targetIterator = targetIterator.parentNode;
      } // end while (targetIterator != ancestor)

      var length = fromAncestorToTarget.length;
      if (length &&
          fromAncestorToTarget.charAt(length - 1) == "/") {
        fromAncestorToTarget = fromAncestorToTarget.substr(0, length - 1);
      }
    } // end if (!(this.searchFlags & nsIXPathGenerator.USE_DESCENDANT_AXIS))

    else {
      // The user wants a path like "//xul:textbox[5]/@value".
      [step, idval] = this.getStepWithoutIndex(targetIterator);

      if (step == "/") {
        // How about that?  We're done.
        return step;
      }

      // Make this a descendant axis.
      step = "descendant::" + step;

      checkPreviousNode = (targetIterator instanceof C_i.nsIDOMText);

      var filterType = checkPreviousNode ?
                       (C_i.nsIDOMNodeFilter.SHOW_TEXT |
                        C_i.nsIDOMNodeFilter.SHOW_CDATA_SECTION) :
                       1 << (targetIterator.nodeType - 1);

      var filter = {
        target: targetIterator,
        compareNodes: this.compareNodes,
        acceptNode: function acceptNode(aNode) {
          return this.compareNodes(this.target, aNode, idval);
        }
      }

      if (!(doc instanceof C_i.nsIDOMDocumentTraversal)) {
        throw ERROR_UNEXPECTED;
      }
      var walker = doc.createTreeWalker(ancestor, filterType, filter, true);
      total = 0;
      index = 0;

      /* We now have the step, but we may need to find the subscript index.
         The target iterator's index represents the ordinal number in brackets
         at the end of a step.  We will only include the ordinal number if it
         is necessary (if the total number of nodes matching this step is
         greater than one).

         However, we do not support adjacent DOM data nodes when we're trying
         to generate a path to the second DOM node. XPath treats CDATA and
         text nodes as the same type of node, so if a text node follows a
         CDATA node immediately (with no element in between, for example), we
         must report a failure.

         If the descendant node list is [text, cdata, comment, text], we cannot
         match the cdata node.  If we're trying to match either
         of the text nodes, we will pass.
       */
      while (walker.nextNode()) {
        indexIterator = walker.currentNode;
        total++;
        prevNode = indexIterator.previousSibling;

        /* Conditions:
           checkPreviousNode: targetIterator is a data node.
           !index: We haven't determined an index yet.
           indexIterator == targetIterator: We're at the node we would
             set an index for.
           prevNode instanceof nsIDOMText: We're not at the first child
             node, and prevNode is a data node.

           If all these conditions are met, we can't absolutely guarantee
           the XPath we generate is correct.
         */
        if (checkPreviousNode &&
            !index &&
            (indexIterator == targetIterator) &&
            (prevNode instanceof C_i.nsIDOMText)) {
            // We don't support two adjacent character data nodes.
          dump("adjacent data nodes!!");
          throw ERROR_FAILURE;
        }

        if (indexIterator == targetIterator) {
          index = total;
        }
      }
      walker = null;

      if (total > 1) {
        step += "[" + index + "]";
      }

      if (fromAncestorToTarget.charAt(0) == "@") {
        fromAncestorToTarget = step + "/" + fromAncestorToTarget;
      } else {
        fromAncestorToTarget = step;
      }
    } // end if (this.searchFlags & nsIXPathGenerator.USE_DESCENDANT_AXIS)

    if (idval) {
      // We got an ID attribute, so context-to-ancestor doesn't mean anything.
      return fromAncestorToTarget;
    }

    // Generate the path from context node to ancestor.
    while (contextIterator != ancestor) {
      if (fromContextToAncestor) {
        fromContextToAncestor += "/";
      }
      fromContextToAncestor += "..";
      contextIterator = contextIterator.parentNode;
    }

    // Join the path from context to ancestor and from ancestor to target.
    if (fromContextToAncestor && fromAncestorToTarget &&
        (fromAncestorToTarget.substr(0, 2) != "//")) {
      fromContextToAncestor += "/";
    }

    return fromContextToAncestor + fromAncestorToTarget;
  },

  // nsIClassInfo
  getInterfaces: function getInterfaces(aCount) {
    var array = [C_i.nsIXPathGenerator,
                 C_i.nsIClassInfo];
    aCount.value = array.length;
    return array;
  },

  // nsIClassInfo
  getHelperForLanguage: function getHelperForLanguage(aLanguage) {
    return null;
  },

  // nsIClassInfo
  contractID: "@mozilla.org/xpath-generator;1",

  // nsIClassInfo
  classDescription: "XPathGenerator",

  // nsIClassInfo
  classID: Components.ID("{f37163a2-596f-4dfe-a2af-3aea378615d5}"),

  // nsIClassInfo
  implementationLanguage: C_i.nsIProgrammingLanguage.JAVASCRIPT,

  // nsIClassInfo
  flags: C_i.nsIClassInfo.DOM_OBJECT | C_i.nsIClassInfo.MAIN_THREAD_ONLY,

  // nsISupports
  QueryInterface: function QueryInterface(aIID) {
    if (aIID.equals(C_i.nsIXPathGenerator) ||
        aIID.equals(C_i.nsIClassInfo) ||
        aIID.equals(C_i.nsISupports))
      return this;

    throw NO_INTERFACE;
  }
};

/* Factories in modules cause leaks according to
   https://bugzilla.mozilla.org/show_bug.cgi?id=168411#c28
 */

var XPathGenerator_factory = {
  createInstance: function createInstance(outer, iid) {
    if (outer != null)
      throw Components.results.NS_ERROR_NO_AGGREGATION;

    var retval = (new XPathGenerator()).QueryInterface(iid);
    return retval;
  }
};

var XPathResolver_factory = {
  createInstance: function createInstance(outer, iid) {
    if (outer != null)
      throw Components.results.NS_ERROR_NO_AGGREGATION;
    return (new XPathResolver()).QueryInterface(iid);
  }
};

/* The rest of this code is largely borrowed from "Creating Applications With
 * Mozilla", http://books.mozdev.org, Chapter 8.
 */
var Module = {
  registerSelf: function registerSelf(compMgr, fileSpec, location, type) {
    const nsIComponentRegistrar = Components.interfaces.nsIComponentRegistrar;
    compReg = compMgr.QueryInterface(nsIComponentRegistrar);
    compReg.registerFactoryLocation(XPathGenerator.prototype.classID,
                                    XPathGenerator.prototype.classDescription,
                                    XPathGenerator.prototype.contractID,
                                    fileSpec,
                                    location,
                                    type);

    compReg.registerFactoryLocation(XPathResolver.prototype.classID,
                                    XPathResolver.prototype.classDescription,
                                    XPathResolver.prototype.contractID,
                                    fileSpec,
                                    location,
                                    type);
  },

  getClassObject: function getClassObject(compMgr, aCID, aIID) {
    const nsIFactory = Components.interfaces.nsIFactory;
    if (!aIID.equals(nsIFactory)) {
      throw Components.results.NS_ERROR_NO_INTERFACE;
    }

    if (aCID.equals(XPathGenerator.prototype.classID)) {
      return XPathGenerator_factory;
    }
    if (aCID.equals(XPathResolver.prototype.classID)) {
      return XPathResolver_factory;
    }

    dump("aCID: " + aCID + "\n");
    throw Components.results.NS_ERROR_NO_INTERFACE;
  },

  canUnload: function canUnload() {
    return true;
  },

  QueryInterface:  function QueryInterface(aIID) {
    const nsIModule   = Components.interfaces.nsIModule;
    const nsISupports = Components.interfaces.nsISupports;
    if (aIID.equals(nsIModule) ||
       (aIID.equals(nsISupports)))
      return this;

    throw Components.results.NS_ERROR_NO_INTERFACE;
  }
};

function NSGetModule(compMgr, fileSpec) {
  return Module;
}
