

DomUtils = {};

(function () {

  var qsaFindAllBySelector = function (selector, contextNode) {
    // If IE7 users report the following error message, you
    // can fix it with "meteor add jquery".
    if (! document.querySelectorAll)
      throw new Error("This browser doesn't support querySelectorAll.");

    // the search is constrained to descendants of `ancestor`,
    // but it doesn't affect the scope of the query.
    var ancestor = contextNode;

    return withElementId(
      contextNode, "DomUtils_findAllBySelector_scope",
      function (idSelector) {
        // scope the entire selector to contextNode by prepending
        // id of contextNode to the selector.
        var doctoredSelector = _.map(selector.split(','), function (selec) {
          return idSelector + " " + selec;
        }).join(',');
        return ancestor.querySelectorAll(doctoredSelector);
      });
  };

  // We have our own, querySelectorAll-based implementation of scoped
  // selector matching; it's all you need in IE 8+ and modern browsers.
  //
  // However, we use Sizzle or jQuery if it's present on the client because of:
  // - apps that want jQuery's selector extensions (:visible, :input, etc.)
  // - apps that include jQuery anyway
  // - apps that want IE 7 support
  //
  // XXX others? zepto?
  var findAllBySelector = (window.Sizzle
                           || (window.jQuery && window.jQuery.find)
                           || qsaFindAllBySelector);

  ///// Common look-up tables used by htmlToFragment et al.

  var testDiv = document.createElement("div");
  testDiv.innerHTML = "   <link/><table></table><select><!----></select>";
  // Need to wrap in a div rather than directly creating SELECT to avoid
  // *another* IE bug.
  var testSelectDiv = document.createElement("div");
  testSelectDiv.innerHTML = "<select><option selected>Foo</option></select>";
  testSelectDiv.firstChild.setAttribute("name", "myname");

  // Tests that, if true, indicate browser quirks present.
  var quirks = {
    // IE loses initial whitespace when setting innerHTML.
    leadingWhitespaceKilled: (testDiv.firstChild.nodeType !== 3),

    // IE may insert an empty tbody tag in a table.
    tbodyInsertion: testDiv.getElementsByTagName("tbody").length > 0,

    // IE loses some tags in some environments (requiring extra wrapper).
    tagsLost: testDiv.getElementsByTagName("link").length === 0,

    // IE <= 9 loses HTML comments in <select> and <option> tags.
    commentsLost: (! testDiv.getElementsByTagName("select")[0].firstChild),

    selectValueMustBeFromAttribute: (testSelectDiv.firstChild.value !== "Foo"),

    // In IE7, setAttribute('name', foo) doesn't show up in rendered HTML.
    // (In FF3, outerHTML is undefined, but it doesn't have this quirk.)
    mustSetNameInCreateElement: (
      testSelectDiv.firstChild.outerHTML &&
        testSelectDiv.firstChild.outerHTML.indexOf("myname") === -1)
  };

  // Set up map of wrappers for different nodes.
  var wrapMap = {
    option: [ 1, "<select multiple='multiple'>", "</select>" ],
    legend: [ 1, "<fieldset>", "</fieldset>" ],
    thead: [ 1, "<table>", "</table>" ],
    tr: [ 2, "<table><tbody>", "</tbody></table>" ],
    td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
    col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
    area: [ 1, "<map>", "</map>" ],
    _default: [ 0, "", "" ]
  };
  _.extend(wrapMap, {
    optgroup: wrapMap.option,
    tbody: wrapMap.thead,
    tfoot: wrapMap.thead,
    colgroup: wrapMap.thead,
    caption: wrapMap.thead,
    th: wrapMap.td
  });
  if (quirks.tagsLost) {
    // trick from jquery.  initial text is ignored when we take lastChild.
    wrapMap._default = [ 1, "div<div>", "</div>" ];
  }

  var rleadingWhitespace = /^\s+/,
      rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,
      rtagName = /<([\w:]+)/,
      rtbody = /<tbody/i,
      rhtml = /<|&#?\w+;/,
      rnoInnerhtml = /<(?:script|style)/i;


  // Parse an HTML string, which may contain multiple top-level tags,
  // and return a DocumentFragment.
  DomUtils.htmlToFragment = function (html) {
    var doc = document; // node factory
    var frag = doc.createDocumentFragment();

    if (! html.length) {
      // empty, do nothing
    } else if (! rhtml.test(html)) {
      // Just text.
      frag.appendChild(doc.createTextNode(html));
    } else {
      // General case.
      // Replace self-closing tags
      html = html.replace(rxhtmlTag, "<$1></$2>");
      // Use first tag to determine wrapping needed.
      var firstTagMatch = rtagName.exec(html);
      var firstTag = (firstTagMatch ? firstTagMatch[1].toLowerCase() : "");
      var wrapData = wrapMap[firstTag] || wrapMap._default;
      var fullHtml = wrapData[1] + html + wrapData[2];
      if (quirks.commentsLost) {
        // rewrite <select> and <option> tags into fake tags
        fullHtml = fullHtml.replace(/<\s*(select|option)\b/ig,
                                    '<ins domutilsrealtagname="$1"');
        fullHtml = fullHtml.replace(/<\/\s*(select|option)\b/ig,
                                    '</ins');
      }

      var container = doc.createElement("div");
      // insert wrapped HTML into a DIV
      container.innerHTML = fullHtml;
      // set "container" to inner node of wrapper
      var unwraps = wrapData[0];
      while (unwraps--) {
        container = container.lastChild;
      }

      if (quirks.tbodyInsertion && ! rtbody.test(html)) {
        // Any tbody we find was created by the browser.
        var tbodies = container.getElementsByTagName("tbody");
        _.each(tbodies, function (n) {
          if (! n.firstChild) {
            // spurious empty tbody
            n.parentNode.removeChild(n);
          }
        });
      }

      if (quirks.leadingWhitespaceKilled) {
        var wsMatch = rleadingWhitespace.exec(html);
        if (wsMatch) {
          container.insertBefore(doc.createTextNode(wsMatch[0]),
                                 container.firstChild);
        }
      }

      if (quirks.commentsLost) {
        // replace fake select tags with real <select> tags
        var fakeTags = [];
        // getElementsByTagName returns a "live" collection, so avoid
        // factorings of this code that iterate over it while mutating
        // the DOM.
        // Here we build an array of fake tags and iterate over that.
        _.each(container.getElementsByTagName("ins"), function (ins) {
          if (ins.getAttribute("domutilsrealtagname")) {
            fakeTags.push(ins);
          }
        });

        _.each(fakeTags, function (fakeTag) {
          var tagName = fakeTag.getAttribute('domutilsrealtagname');
          if (quirks.mustSetNameInCreateElement &&
              fakeTag.getAttribute('name')) {
            // IE7 can't set 'name' with setAttribute, but it has this
            // crazy syntax for setting it at create time.
            // http://webbugtrack.blogspot.com/2007/10/bug-235-createelement-is-broken-in-ie.html
            // http://msdn.microsoft.com/en-us/library/ms536389.aspx
            tagName = "<" + tagName + " name='" +
              _.escape(fakeTag.getAttribute('name')) + "'/>";
          }
          var realTag = document.createElement(tagName);
          fakeTag.removeAttribute('domutilsrealtagname');
          // copy all attributes. for some reason mergeAttributes doesn't work
          // here: eg, it doesn't copy SELECTED or VALUE. (Probably because
          // these attributes would be expando on INS?)
          var fakeAttrs = fakeTag.attributes;
          for (var i = 0; i < fakeAttrs.length; ++i) {
            var fakeAttr = fakeAttrs.item(i);
            if (fakeAttr.specified) {
              var name = fakeAttr.name.toLowerCase();
              var value = String(fakeAttr.value);
              // IE7 gets confused if you try to setAttribute('selected', ''),
              // so be a little more explicit.
              if (name === 'selected' && value === '')
                value = 'selected';
              realTag.setAttribute(name, value);
            }
          }

          // move all children
          while (fakeTag.firstChild)
            realTag.appendChild(fakeTag.firstChild);
          // replace
          fakeTag.parentNode.replaceChild(realTag, fakeTag);
        });
      }

      // Reparent children of container to frag.
      while (container.firstChild)
        frag.appendChild(container.firstChild);
    }

    return frag;
  };

  // Return an HTML string representing the contents of frag,
  // a DocumentFragment.  (This is what innerHTML would do if
  // it were defined on DocumentFragments.)
  DomUtils.fragmentToHtml = function (frag) {
    frag = frag.cloneNode(true); // deep copy, don't touch original!

    return DomUtils.fragmentToContainer(frag).innerHTML;
  };

  // Given a DocumentFragment, return a node whose children are the
  // reparented contents of the DocumentFragment.  In most cases this
  // is as simple as creating a DIV, but in the case of a fragment
  // containing TRs, for example, it's necessary to create a TABLE and
  // a TBODY and return the TBODY.
  DomUtils.fragmentToContainer = function (frag) {
    var doc = document; // node factory

    var firstElement = frag.firstChild;
    while (firstElement && firstElement.nodeType !== 1) {
      firstElement = firstElement.nextSibling;
    }

    var container = doc.createElement("div");

    if (! firstElement) {
      // no tags!
      container.appendChild(frag);
    } else {
      var firstTag = firstElement.nodeName;
      var wrapData = wrapMap[firstTag] || wrapMap._default;

      container.innerHTML = wrapData[1] + wrapData[2];
      var unwraps = wrapData[0];
      while (unwraps--) {
        container = container.lastChild;
      }

      container.appendChild(frag);
    }

    return container;
  };

  // Returns true if element a contains node b and is not node b.
  DomUtils.elementContains = function (a, b) {
    if (a.nodeType !== 1) /* ELEMENT */
      return false;
    if (a === b)
      return false;

    if (a.compareDocumentPosition) {
      return a.compareDocumentPosition(b) & 0x10;
    } else {
      // Should be only old IE and maybe other old browsers here.
      // Modern Safari has both functions but seems to get contains() wrong.
      // IE can't handle b being a text node.  We work around this
      // by doing a direct parent test now.
      b = b.parentNode;
      if (! (b && b.nodeType === 1)) /* ELEMENT */
        return false;
      if (a === b)
        return true;

      return a.contains(b);
    }
  };

  // Returns an array containing the children of contextNode that
  // match `selector`. Unlike querySelectorAll, `selector` is
  // interpreted as if the document were rooted at `contextNode` --
  // the only nodes that can be used to match components of the
  // selector are the descendents of `contextNode`. `contextNode`
  // itself is not included (it can't be used to match a component of
  // the selector, and it can never be included in the returned
  // array.)
  //
  // `contextNode` may be either a node, a document, or a DocumentFragment.
  DomUtils.findAll = function (contextNode, selector) {
    if (contextNode.nodeType === 11 /* DocumentFragment */) {
      // contextNode is a DocumentFragment.
      //
      // We don't expect to be able to run selectors on a DocumentFragment
      // (Sizzle won't work) but we can on a normal elements that aren't
      // in the document.  Fortunately we can manipulate offscreen nodes
      // as much as we want as long as we put them back the way they were
      // when we're done.
      var frag = contextNode;
      var container = DomUtils.fragmentToContainer(frag);
      var results = findAllBySelector(selector, container);
      // put nodes back into frag
      while (container.firstChild)
        frag.appendChild(container.firstChild);
      return results;
    }

    return findAllBySelector(selector, contextNode);
  };

  // Like `findAll` but finds one element (or returns null).
  DomUtils.find = function (contextNode, selector) {
    var results = DomUtils.findAll(contextNode, selector);
    return (results.length ? results[0] : null);
  };

  var isElementInClipRange = function (elem, clipStart, clipEnd) {
    // elem is not in clip range if it contains the clip range
    if (DomUtils.elementContains(elem, clipStart))
      return false;
    // elem is in clip range if clipStart <= elem <= clipEnd
    return (DomUtils.compareElementIndex(clipStart, elem) <= 0) &&
      (DomUtils.compareElementIndex(elem, clipEnd) <= 0);
  };

  // Like `findAll` but searches the nodes from `start` to `end`
  // inclusive. `start` and `end` must be siblings, and they participate
  // in the search (they can be used to match selector components, and
  // they can appear in the returned results). It's as if the parent of
  // `start` and `end` serves as contextNode, but matches from children
  // that aren't between `start` and `end` (inclusive) are ignored.
  //
  // If `selector` involves sibling selectors, child index selectors, or
  // the like, the results are undefined.
  //
  // precond: clipStart/clipEnd are descendents of contextNode
  // XXX document
  DomUtils.findAllClipped = function (contextNode, selector, clipStart, clipEnd) {

    // Ensure the clip range starts and ends on element nodes.  This is possible
    // to do without changing the result set because non-element nodes can't
    // be or contain matches.
    while (clipStart !== clipEnd && clipStart.nodeType !== 1)
      clipStart = clipStart.nextSibling;
    while (clipStart !== clipEnd && clipEnd.nodeType !== 1)
      clipEnd = clipEnd.previousSibling;
    if (clipStart.nodeType !== 1)
      return []; // no top-level elements!  start === end and it's not an element

    // resultsPlus includes matches all matches descended from contextNode,
    // including those that aren't in the clip range.
    var resultsPlus = DomUtils.findAll(contextNode, selector);

    // Filter the list of nodes to remove nodes that occur before start
    // or after end.
    return _.reject(resultsPlus, function (n) {
      return ! isElementInClipRange(n, clipStart, clipEnd);
    });
  };

  // Like `findAllClipped` but finds one element (or returns null).
  DomUtils.findClipped = function (contextNode, selector, clipStart, clipEnd) {
    var results = DomUtils.findAllClipped(
      contextNode, selector, clipStart, clipEnd);
    return (results.length ? results[0] : null);
  };

  // Executes `func` while ensuring that `element` has an ID.  If `element`
  // doesn't have an ID, it is assigned `magicId` temporarily.
  // Calls func with a selector of the form "[id='...']" as an argument.
  var withElementId = function (element, magicId, func) {
    var didSetId = false;
    if (! element.id) {
      element.setAttribute('id', magicId);
      didSetId = true;
    }
    try {
      var escapedNodeId = element.id.replace(/'/g, "\\$&");
      return func("[id='" + escapedNodeId + "']");
    } finally {
      if (didSetId)
        element.removeAttribute('id');
    }
  };

  var matchesSelectorMaybeClipped = function (element, contextNode, selector,
                                             clipStart, clipEnd) {
    var selecs = selector.split(',');
    for(var i = 0, N = selecs.length; i < N; i++) {
      var matches = withElementId(
        element, "DomUtils_matchesSelector_target",
        function (idSelector) {
          var trimmedSelector = selector.match(/\S.*?(?=\s*$)/)[0];
          // appending [id='foo'] to a selector with no whitespace ought to
          // simply restrict the set of possible outputs regardless of the
          // form of the selector.
          var doctoredSelector = trimmedSelector + idSelector;
          var result;
          if (clipStart)
            result = DomUtils.findClipped(contextNode, doctoredSelector,
                                          clipStart, clipEnd);
          else
            result = DomUtils.find(contextNode, doctoredSelector);
          return (result === element);
        });

      if (matches)
        return true;
    }

    return false;
  };

  // Check if `element` matches `selector`, scoped to `contextNode`.
  DomUtils.matchesSelector = function (element, contextNode, selector) {
    return matchesSelectorMaybeClipped(element, contextNode, selector);
  };

  // Check if `element` matches `selector`, scoped to `contextNode`,
  // clipped to ordered siblings `clipStart`..`clipEnd`.
  DomUtils.matchesSelectorClipped = function (element, contextNode, selector,
                                              clipStart, clipEnd) {
    return matchesSelectorMaybeClipped(element, contextNode, selector,
                                       clipStart, clipEnd);
  };

  // Returns 0 if the nodes are the same or either one contains the other;
  // otherwise, -1 if a comes before b, or else 1 if b comes before a in
  // document order.
  // Requires: `a` and `b` are element nodes in the same document tree.
  DomUtils.compareElementIndex = function (a, b) {
    // See http://ejohn.org/blog/comparing-document-position/
    if (a === b)
      return 0;
    if (a.compareDocumentPosition) {
      var n = a.compareDocumentPosition(b);
      return ((n & 0x18) ? 0 : ((n & 0x4) ? -1 : 1));
    } else {
      // Only old IE is known to not have compareDocumentPosition (though Safari
      // originally lacked it).  Thankfully, IE gives us a way of comparing elements
      // via the "sourceIndex" property.
      if (a.contains(b) || b.contains(a))
        return 0;
      return (a.sourceIndex < b.sourceIndex ? -1 : 1);
    }
  };

  // Wrap `frag` as necessary to prepare it for insertion in
  // `container`. For example, if `frag` has TR nodes at top level,
  // and `container` is a TABLE, then it's necessary to wrap `frag` in
  // a TBODY to avoid IE quirks.
  //
  // `frag` is a DocumentFragment and will be modified in
  // place. `container` is a DOM element.
  DomUtils.wrapFragmentForContainer = function (frag, container) {
    if (container && container.nodeName === "TABLE" &&
        _.any(frag.childNodes,
              function (n) { return n.nodeName === "TR"; })) {
      // Avoid putting a TR directly in a TABLE without an
      // intervening TBODY, because it doesn't work in IE.  We do
      // the same thing on all browsers for ease of testing
      // and debugging.
      var tbody = document.createElement("TBODY");
      tbody.appendChild(frag);
      frag.appendChild(tbody);
    }
  };

  // Return true if `node` is part of the global DOM document. Like
  // elementContains(document, node), except (1) it works for any node
  // (eg, text nodes), not just elements; (2) it works around browser
  // quirks that would otherwise come up when passing 'document' as
  // the first argument to elementContains.
  //
  // Returns true if node === document.
  DomUtils.isInDocument = function (node) {
    // Deal with all cases where node is not an element
    // node descending from the body first...
    if (node === document)
      return true;

    if (node.nodeType !== 1 /* Element */)
      node = node.parentNode;
    if (! (node && node.nodeType === 1))
      return false;
    if (node === document.body)
      return true;

    return DomUtils.elementContains(document.body, node);
  };

  // Return an HTML string representation of the nodes from
  // firstNode to lastNode, which must be siblings.
  // The tags representing firstNode and lastNode are included,
  // but not their parent or outer siblings.
  DomUtils.rangeToHtml = function (firstNode, lastNode) {
    var frag = document.createDocumentFragment();
    for(var n = firstNode, after = lastNode.nextSibling;
        n && n !== after;
        n = n.nextSibling)
      frag.appendChild(n.cloneNode(true)); // deep copy
    return DomUtils.fragmentToHtml(frag);
  };

  // Return an HTML string representation of node, including its
  // own open and close tag.
  DomUtils.outerHtml = function (node) {
    return DomUtils.rangeToHtml(node, node);
  };

  // Sets the value of an element, portably across browsers. There's a special
  // case for SELECT elements in IE.
  DomUtils.setElementValue = function (node, value) {
    // Try to assign the value.
    node.value = value;
    if (node.value === value || node.nodeName !== 'SELECT')
      return;

    // IE (all versions) appears to only let you assign SELECT values which
    // match valid OPTION values... and moreover, the OPTION value must be
    // explicitly given as an attribute, not just as the text. So we hunt for
    // the OPTION and select it.
    var options = DomUtils.findAll(node, 'option');
    for (var i = 0; i < options.length; ++i) {
      if (DomUtils.getElementValue(options[i]) === value) {
        options[i].selected = true;
        return;
      }
    }
  };

  // Gets the value of an element, portably across browsers. There's a special
  // case for SELECT elements in IE.
  DomUtils.getElementValue = function (node) {
    if (!quirks.selectValueMustBeFromAttribute)
      return node.value;

    if (node.nodeName === 'OPTION') {
      // Inspired by jQuery.valHooks.option.get.
      var val = node.attributes.value;
      return !val || val.specified ? node.value : node.text;
    } else if (node.nodeName === 'SELECT') {
      if (node.selectedIndex < 0)
        return null;
      return DomUtils.getElementValue(node.options[node.selectedIndex]);
    } else {
      return node.value;
    }
  };

})();
