'use strict';
module.exports = Element;

var xml = require('./xmlnames');
var utils = require('./utils');
var NAMESPACE = utils.NAMESPACE;
var attributes = require('./attributes');
var Node = require('./Node');
var NodeList = require('./NodeList');
var NodeUtils = require('./NodeUtils');
var FilteredElementList = require('./FilteredElementList');
var DOMException = require('./DOMException');
var DOMTokenList = require('./DOMTokenList');
var select = require('./select');
var ContainerNode = require('./ContainerNode');
var ChildNode = require('./ChildNode');
var NonDocumentTypeChildNode = require('./NonDocumentTypeChildNode');
var NamedNodeMap = require('./NamedNodeMap');

var uppercaseCache = Object.create(null);

function Element(doc, localName, namespaceURI, prefix) {
  ContainerNode.call(this);
  this.nodeType = Node.ELEMENT_NODE;
  this.ownerDocument = doc;
  this.localName = localName;
  this.namespaceURI = namespaceURI;
  this.prefix = prefix;
  this._tagName = undefined;

  // These properties maintain the set of attributes
  this._attrsByQName = Object.create(null); // The qname->Attr map
  this._attrsByLName = Object.create(null); // The ns|lname->Attr map
  this._attrKeys = []; // attr index -> ns|lname
}

function recursiveGetText(node, a) {
  if (node.nodeType === Node.TEXT_NODE) {
    a.push(node._data);
  } else {
    for (var i = 0, n = node.childNodes.length; i < n; i++) recursiveGetText(node.childNodes[i], a);
  }
}

Element.prototype = Object.create(ContainerNode.prototype, {
  isHTML: {
    get: function isHTML() {
      return this.namespaceURI === NAMESPACE.HTML && this.ownerDocument.isHTML;
    },
  },
  tagName: {
    get: function tagName() {
      if (this._tagName === undefined) {
        var tn;
        if (this.prefix === null) {
          tn = this.localName;
        } else {
          tn = this.prefix + ':' + this.localName;
        }
        if (this.isHTML) {
          var up = uppercaseCache[tn];
          if (!up) {
            // Converting to uppercase can be slow, so cache the conversion.
            uppercaseCache[tn] = up = utils.toASCIIUpperCase(tn);
          }
          tn = up;
        }
        this._tagName = tn;
      }
      return this._tagName;
    },
  },
  nodeName: {
    get: function () {
      return this.tagName;
    },
  },
  nodeValue: {
    get: function () {
      return null;
    },
    set: function () {},
  },
  textContent: {
    get: function () {
      var strings = [];
      recursiveGetText(this, strings);
      return strings.join('');
    },
    set: function (newtext) {
      this.removeChildren();
      if (newtext !== null && newtext !== undefined && newtext !== '') {
        this._appendChild(this.ownerDocument.createTextNode(newtext));
      }
    },
  },
  innerHTML: {
    get: function () {
      return this.serialize();
    },
    set: function(v) {
      var parser = this.ownerDocument.implementation.mozHTMLParser(
        this.ownerDocument._address,
        this);
      parser.parse(v===null ? '' : String(v), true);

      // Remove any existing children of this node
      var target = this;
      while(target.hasChildNodes())
        target.removeChild(target.firstChild);

      // Now copy newly parsed children to this node
      target.appendChild(parser._asDocumentFragment());

    },
  },
  outerHTML: {
    get: function () {
      // "the attribute must return the result of running the HTML fragment
      // serialization algorithm on a fictional node whose only child is
      // the context object"
      //
      // The serialization logic is intentionally implemented in a separate
      // `NodeUtils` helper instead of the more obvious choice of a private
      // `_serializeOne()` method on the `Node.prototype` in order to avoid
      // the megamorphic `this._serializeOne` property access, which reduces
      // performance unnecessarily. If you need specialized behavior for a
      // certain subclass, you'll need to implement that in `NodeUtils`.
      // See https://github.com/fgnass/domino/pull/142 for more information.
      return NodeUtils.serializeOne(this, { nodeType: 0 });
    },
    set: function (v) {
      var document = this.ownerDocument;
      var parent = this.parentNode;
      if (parent === null) {
        return;
      }
      if (parent.nodeType === Node.DOCUMENT_NODE) {
        utils.NoModificationAllowedError();
      }
      if (parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
        parent = parent.ownerDocument.createElement('body');
      }
      var parser = document.implementation.mozHTMLParser(document._address, parent);
      parser.parse(v === null ? '' : String(v), true);
      this.replaceWith(parser._asDocumentFragment());
    },
  },

  _insertAdjacent: {
    value: function _insertAdjacent(position, node) {
      var first = false;
      switch (position) {
        case 'beforebegin':
          first = true;
        /* falls through */
        case 'afterend':
          var parent = this.parentNode;
          if (parent === null) {
            return null;
          }
          return parent.insertBefore(node, first ? this : this.nextSibling);
        case 'afterbegin':
          first = true;
        /* falls through */
        case 'beforeend':
          return this.insertBefore(node, first ? this.firstChild : null);
        default:
          return utils.SyntaxError();
      }
    },
  },

  insertAdjacentElement: {
    value: function insertAdjacentElement(position, element) {
      if (element.nodeType !== Node.ELEMENT_NODE) {
        throw new TypeError('not an element');
      }
      position = utils.toASCIILowerCase(String(position));
      return this._insertAdjacent(position, element);
    },
  },

  insertAdjacentText: {
    value: function insertAdjacentText(position, data) {
      var textNode = this.ownerDocument.createTextNode(data);
      position = utils.toASCIILowerCase(String(position));
      this._insertAdjacent(position, textNode);
      // "This method returns nothing because it existed before we had a chance
      // to design it."
    },
  },

  insertAdjacentHTML: {
    value: function insertAdjacentHTML(position, text) {
      position = utils.toASCIILowerCase(String(position));
      text = String(text);
      var context;
      switch (position) {
        case 'beforebegin':
        case 'afterend':
          context = this.parentNode;
          if (context === null || context.nodeType === Node.DOCUMENT_NODE) {
            utils.NoModificationAllowedError();
          }
          break;
        case 'afterbegin':
        case 'beforeend':
          context = this;
          break;
        default:
          utils.SyntaxError();
      }
      if (
        !(context instanceof Element) ||
        (context.ownerDocument.isHTML &&
          context.localName === 'html' &&
          context.namespaceURI === NAMESPACE.HTML)
      ) {
        context = context.ownerDocument.createElementNS(NAMESPACE.HTML, 'body');
      }
      var parser = this.ownerDocument.implementation.mozHTMLParser(
        this.ownerDocument._address,
        context
      );
      parser.parse(text, true);
      this._insertAdjacent(position, parser._asDocumentFragment());
    },
  },

  children: {
    get: function () {
      if (!this._children) {
        this._children = new ChildrenCollection(this);
      }
      return this._children;
    },
  },

  attributes: {
    get: function () {
      if (!this._attributes) {
        this._attributes = new AttributesArray(this);
      }
      return this._attributes;
    },
  },

  firstElementChild: {
    get: function () {
      for (var kid = this.firstChild; kid !== null; kid = kid.nextSibling) {
        if (kid.nodeType === Node.ELEMENT_NODE) return kid;
      }
      return null;
    },
  },

  lastElementChild: {
    get: function () {
      for (var kid = this.lastChild; kid !== null; kid = kid.previousSibling) {
        if (kid.nodeType === Node.ELEMENT_NODE) return kid;
      }
      return null;
    },
  },

  childElementCount: {
    get: function () {
      return this.children.length;
    },
  },

  // Return the next element, in source order, after this one or
  // null if there are no more.  If root element is specified,
  // then don't traverse beyond its subtree.
  //
  // This is not a DOM method, but is convenient for
  // lazy traversals of the tree.
  nextElement: {
    value: function (root) {
      if (!root) root = this.ownerDocument.documentElement;
      var next = this.firstElementChild;
      if (!next) {
        // don't use sibling if we're at root
        if (this === root) return null;
        next = this.nextElementSibling;
      }
      if (next) return next;

      // If we can't go down or across, then we have to go up
      // and across to the parent sibling or another ancestor's
      // sibling.  Be careful, though: if we reach the root
      // element, or if we reach the documentElement, then
      // the traversal ends.
      for (
        var parent = this.parentElement;
        parent && parent !== root;
        parent = parent.parentElement
      ) {
        next = parent.nextElementSibling;
        if (next) return next;
      }

      return null;
    },
  },

  // XXX:
  // Tests are currently failing for this function.
  // Awaiting resolution of:
  // http://lists.w3.org/Archives/Public/www-dom/2011JulSep/0016.html
  getElementsByTagName: {
    value: function getElementsByTagName(lname) {
      var filter;
      if (!lname) return new NodeList();
      if (lname === '*')
        filter = function () {
          return true;
        };
      else if (this.isHTML) filter = htmlLocalNameElementFilter(lname);
      else filter = localNameElementFilter(lname);

      return new FilteredElementList(this, filter);
    },
  },

  getElementsByTagNameNS: {
    value: function getElementsByTagNameNS(ns, lname) {
      var filter;
      if (ns === '*' && lname === '*')
        filter = function () {
          return true;
        };
      else if (ns === '*') filter = localNameElementFilter(lname);
      else if (lname === '*') filter = namespaceElementFilter(ns);
      else filter = namespaceLocalNameElementFilter(ns, lname);

      return new FilteredElementList(this, filter);
    },
  },

  getElementsByClassName: {
    value: function getElementsByClassName(names) {
      names = String(names).trim();
      if (names === '') {
        var result = new NodeList(); // Empty node list
        return result;
      }
      names = names.split(/[ \t\r\n\f]+/); // Split on ASCII whitespace
      return new FilteredElementList(this, classNamesElementFilter(names));
    },
  },

  getElementsByName: {
    value: function getElementsByName(name) {
      return new FilteredElementList(this, elementNameFilter(String(name)));
    },
  },

  // Utility methods used by the public API methods above
  clone: {
    value: function clone() {
      var e;

      // XXX:
      // Modify this to use the constructor directly or
      // avoid error checking in some other way. In case we try
      // to clone an invalid node that the parser inserted.
      //
      if (this.namespaceURI !== NAMESPACE.HTML || this.prefix || !this.ownerDocument.isHTML) {
        e = this.ownerDocument.createElementNS(
          this.namespaceURI,
          this.prefix !== null ? this.prefix + ':' + this.localName : this.localName
        );
      } else {
        e = this.ownerDocument.createElement(this.localName);
      }

      for (var i = 0, n = this._attrKeys.length; i < n; i++) {
        var lname = this._attrKeys[i];
        var a = this._attrsByLName[lname];
        var b = a.cloneNode();
        b._setOwnerElement(e);
        e._attrsByLName[lname] = b;
        e._addQName(b);
      }
      e._attrKeys = this._attrKeys.concat();

      return e;
    },
  },

  isEqual: {
    value: function isEqual(that) {
      if (
        this.localName !== that.localName ||
        this.namespaceURI !== that.namespaceURI ||
        this.prefix !== that.prefix ||
        this._numattrs !== that._numattrs
      )
        return false;

      // Compare the sets of attributes, ignoring order
      // and ignoring attribute prefixes.
      for (var i = 0, n = this._numattrs; i < n; i++) {
        var a = this._attr(i);
        if (!that.hasAttributeNS(a.namespaceURI, a.localName)) return false;
        if (that.getAttributeNS(a.namespaceURI, a.localName) !== a.value) return false;
      }

      return true;
    },
  },

  // This is the 'locate a namespace prefix' algorithm from the
  // DOM specification.  It is used by Node.lookupPrefix()
  // (Be sure to compare DOM3 and DOM4 versions of spec.)
  _lookupNamespacePrefix: {
    value: function _lookupNamespacePrefix(ns, originalElement) {
      if (
        this.namespaceURI &&
        this.namespaceURI === ns &&
        this.prefix !== null &&
        originalElement.lookupNamespaceURI(this.prefix) === ns
      ) {
        return this.prefix;
      }

      for (var i = 0, n = this._numattrs; i < n; i++) {
        var a = this._attr(i);
        if (
          a.prefix === 'xmlns' &&
          a.value === ns &&
          originalElement.lookupNamespaceURI(a.localName) === ns
        ) {
          return a.localName;
        }
      }

      var parent = this.parentElement;
      return parent ? parent._lookupNamespacePrefix(ns, originalElement) : null;
    },
  },

  // This is the 'locate a namespace' algorithm for Element nodes
  // from the DOM Core spec.  It is used by Node#lookupNamespaceURI()
  lookupNamespaceURI: {
    value: function lookupNamespaceURI(prefix) {
      if (prefix === '' || prefix === undefined) {
        prefix = null;
      }
      if (this.namespaceURI !== null && this.prefix === prefix) return this.namespaceURI;

      for (var i = 0, n = this._numattrs; i < n; i++) {
        var a = this._attr(i);
        if (a.namespaceURI === NAMESPACE.XMLNS) {
          if (
            (a.prefix === 'xmlns' && a.localName === prefix) ||
            (prefix === null && a.prefix === null && a.localName === 'xmlns')
          ) {
            return a.value || null;
          }
        }
      }

      var parent = this.parentElement;
      return parent ? parent.lookupNamespaceURI(prefix) : null;
    },
  },

  //
  // Attribute handling methods and utilities
  //

  /*
   * Attributes in the DOM are tricky:
   *
   * - there are the 8 basic get/set/has/removeAttribute{NS} methods
   *
   * - but many HTML attributes are also 'reflected' through IDL
   *   attributes which means that they can be queried and set through
   *   regular properties of the element.  There is just one attribute
   *   value, but two ways to get and set it.
   *
   * - Different HTML element types have different sets of reflected
     attributes.
   *
   * - attributes can also be queried and set through the .attributes
   *   property of an element.  This property behaves like an array of
   *   Attr objects.  The value property of each Attr is writeable, so
   *   this is a third way to read and write attributes.
   *
   * - for efficiency, we really want to store attributes in some kind
   *   of name->attr map.  But the attributes[] array is an array, not a
   *   map, which is kind of unnatural.
   *
   * - When using namespaces and prefixes, and mixing the NS methods
   *   with the non-NS methods, it is apparently actually possible for
   *   an attributes[] array to have more than one attribute with the
   *   same qualified name.  And certain methods must operate on only
   *   the first attribute with such a name.  So for these methods, an
   *   inefficient array-like data structure would be easier to
   *   implement.
   *
   * - The attributes[] array is live, not a snapshot, so changes to the
   *   attributes must be immediately visible through existing arrays.
   *
   * - When attributes are queried and set through IDL properties
   *   (instead of the get/setAttributes() method or the attributes[]
   *   array) they may be subject to type conversions, URL
   *   normalization, etc., so some extra processing is required in that
   *   case.
   *
   * - But access through IDL properties is probably the most common
   *   case, so we'd like that to be as fast as possible.
   *
   * - We can't just store attribute values in their parsed idl form,
   *   because setAttribute() has to return whatever string is passed to
   *   getAttribute even if it is not a legal, parseable value. So
   *   attribute values must be stored in unparsed string form.
   *
   * - We need to be able to send change notifications or mutation
   *   events of some sort to the renderer whenever an attribute value
   *   changes, regardless of the way in which it changes.
   *
   * - Some attributes, such as id and class affect other parts of the
   *   DOM API, like getElementById and getElementsByClassName and so
   *   for efficiency, we need to specially track changes to these
   *   special attributes.
   *
   * - Some attributes like class have different names (className) when
   *   reflected.
   *
   * - Attributes whose names begin with the string 'data-' are treated
     specially.
   *
   * - Reflected attributes that have a boolean type in IDL have special
   *   behavior: setting them to false (in IDL) is the same as removing
   *   them with removeAttribute()
   *
   * - numeric attributes (like HTMLElement.tabIndex) can have default
   *   values that must be returned by the idl getter even if the
   *   content attribute does not exist. (The default tabIndex value
   *   actually varies based on the type of the element, so that is a
   *   tricky one).
   *
   * See
   * http://www.whatwg.org/specs/web-apps/current-work/multipage/urls.html#reflect
   * for rules on how attributes are reflected.
   *
   */

  getAttribute: {
    value: function getAttribute(qname) {
      var attr = this.getAttributeNode(qname);
      return attr ? attr.value : null;
    },
  },

  getAttributeNS: {
    value: function getAttributeNS(ns, lname) {
      var attr = this.getAttributeNodeNS(ns, lname);
      return attr ? attr.value : null;
    },
  },

  getAttributeNode: {
    value: function getAttributeNode(qname) {
      qname = String(qname);
      if (/[A-Z]/.test(qname) && this.isHTML) qname = utils.toASCIILowerCase(qname);
      var attr = this._attrsByQName[qname];
      if (!attr) return null;

      if (Array.isArray(attr))
        // If there is more than one
        attr = attr[0]; // use the first

      return attr;
    },
  },

  getAttributeNodeNS: {
    value: function getAttributeNodeNS(ns, lname) {
      ns = ns === undefined || ns === null ? '' : String(ns);
      lname = String(lname);
      var attr = this._attrsByLName[ns + '|' + lname];
      return attr ? attr : null;
    },
  },

  hasAttribute: {
    value: function hasAttribute(qname) {
      qname = String(qname);
      if (/[A-Z]/.test(qname) && this.isHTML) qname = utils.toASCIILowerCase(qname);
      return this._attrsByQName[qname] !== undefined;
    },
  },

  hasAttributeNS: {
    value: function hasAttributeNS(ns, lname) {
      ns = ns === undefined || ns === null ? '' : String(ns);
      lname = String(lname);
      var key = ns + '|' + lname;
      return this._attrsByLName[key] !== undefined;
    },
  },

  hasAttributes: {
    value: function hasAttributes() {
      return this._numattrs > 0;
    },
  },

  toggleAttribute: {
    value: function toggleAttribute(qname, force) {
      qname = String(qname);
      if (!xml.isValidName(qname)) utils.InvalidCharacterError();
      if (/[A-Z]/.test(qname) && this.isHTML) qname = utils.toASCIILowerCase(qname);
      var a = this._attrsByQName[qname];
      if (a === undefined) {
        if (force === undefined || force === true) {
          this._setAttribute(qname, '');
          return true;
        }
        return false;
      } else {
        if (force === undefined || force === false) {
          this.removeAttribute(qname);
          return false;
        }
        return true;
      }
    },
  },

  // Set the attribute without error checking. The parser uses this.
  _setAttribute: {
    value: function _setAttribute(qname, value) {
      // XXX: the spec says that this next search should be done
      // on the local name, but I think that is an error.
      // email pending on www-dom about it.
      var attr = this._attrsByQName[qname];
      var isnew;
      if (!attr) {
        attr = this._newattr(qname);
        isnew = true;
      } else {
        if (Array.isArray(attr)) attr = attr[0];
      }

      // Now set the attribute value on the new or existing Attr object.
      // The Attr.value setter method handles mutation events, etc.
      attr.value = value;
      if (this._attributes) this._attributes[qname] = attr;
      if (isnew && this._newattrhook) this._newattrhook(qname, value);
    },
  },

  // Check for errors, and then set the attribute
  setAttribute: {
    value: function setAttribute(qname, value) {
      qname = String(qname);
      if (!xml.isValidName(qname)) utils.InvalidCharacterError();
      if (/[A-Z]/.test(qname) && this.isHTML) qname = utils.toASCIILowerCase(qname);
      this._setAttribute(qname, String(value));
    },
  },

  // The version with no error checking used by the parser
  _setAttributeNS: {
    value: function _setAttributeNS(ns, qname, value) {
      var pos = qname.indexOf(':'),
        prefix,
        lname;
      if (pos < 0) {
        prefix = null;
        lname = qname;
      } else {
        prefix = qname.substring(0, pos);
        lname = qname.substring(pos + 1);
      }

      if (ns === '' || ns === undefined) ns = null;
      var key = (ns === null ? '' : ns) + '|' + lname;

      var attr = this._attrsByLName[key];
      var isnew;
      if (!attr) {
        attr = new Attr(this, lname, prefix, ns);
        isnew = true;
        this._attrsByLName[key] = attr;
        if (this._attributes) {
          this._attributes[this._attrKeys.length] = attr;
        }
        this._attrKeys.push(key);

        // We also have to make the attr searchable by qname.
        // But we have to be careful because there may already
        // be an attr with this qname.
        this._addQName(attr);
      } else if (false /* changed in DOM 4 */) {
        // Calling setAttributeNS() can change the prefix of an
        // existing attribute in DOM 2/3.
        if (attr.prefix !== prefix) {
          // Unbind the old qname
          this._removeQName(attr);
          // Update the prefix
          attr.prefix = prefix;
          // Bind the new qname
          this._addQName(attr);
        }
      }
      attr.value = value; // Automatically sends mutation event
      if (isnew && this._newattrhook) this._newattrhook(qname, value);
    },
  },

  // Do error checking then call _setAttributeNS
  setAttributeNS: {
    value: function setAttributeNS(ns, qname, value) {
      // Convert parameter types according to WebIDL
      ns = ns === null || ns === undefined || ns === '' ? null : String(ns);
      qname = String(qname);
      if (!xml.isValidQName(qname)) utils.InvalidCharacterError();

      var pos = qname.indexOf(':');
      var prefix = pos < 0 ? null : qname.substring(0, pos);

      if (
        (prefix !== null && ns === null) ||
        (prefix === 'xml' && ns !== NAMESPACE.XML) ||
        ((qname === 'xmlns' || prefix === 'xmlns') && ns !== NAMESPACE.XMLNS) ||
        (ns === NAMESPACE.XMLNS && !(qname === 'xmlns' || prefix === 'xmlns'))
      )
        utils.NamespaceError();

      this._setAttributeNS(ns, qname, String(value));
    },
  },

  setAttributeNode: {
    value: function setAttributeNode(attr) {
      if (attr.ownerElement !== null && attr.ownerElement !== this) {
        throw new DOMException(DOMException.INUSE_ATTRIBUTE_ERR);
      }
      var result = null;
      var oldAttrs = this._attrsByQName[attr.name];
      if (oldAttrs) {
        if (!Array.isArray(oldAttrs)) {
          oldAttrs = [oldAttrs];
        }
        if (
          oldAttrs.some(function (a) {
            return a === attr;
          })
        ) {
          return attr;
        } else if (attr.ownerElement !== null) {
          throw new DOMException(DOMException.INUSE_ATTRIBUTE_ERR);
        }
        oldAttrs.forEach(function (a) {
          this.removeAttributeNode(a);
        }, this);
        result = oldAttrs[0];
      }
      this.setAttributeNodeNS(attr);
      return result;
    },
  },

  setAttributeNodeNS: {
    value: function setAttributeNodeNS(attr) {
      if (attr.ownerElement !== null) {
        throw new DOMException(DOMException.INUSE_ATTRIBUTE_ERR);
      }
      var ns = attr.namespaceURI;
      var key = (ns === null ? '' : ns) + '|' + attr.localName;
      var oldAttr = this._attrsByLName[key];
      if (oldAttr) {
        this.removeAttributeNode(oldAttr);
      }
      attr._setOwnerElement(this);
      this._attrsByLName[key] = attr;
      if (this._attributes) {
        this._attributes[this._attrKeys.length] = attr;
      }
      this._attrKeys.push(key);
      this._addQName(attr);
      if (this._newattrhook) this._newattrhook(attr.name, attr.value);
      return oldAttr || null;
    },
  },

  removeAttribute: {
    value: function removeAttribute(qname) {
      qname = String(qname);
      if (/[A-Z]/.test(qname) && this.isHTML) qname = utils.toASCIILowerCase(qname);

      var attr = this._attrsByQName[qname];
      if (!attr) return;

      // If there is more than one match for this qname
      // so don't delete the qname mapping, just remove the first
      // element from it.
      if (Array.isArray(attr)) {
        if (attr.length > 2) {
          attr = attr.shift(); // remove it from the array
        } else {
          this._attrsByQName[qname] = attr[1];
          attr = attr[0];
        }
      } else {
        // only a single match, so remove the qname mapping
        this._attrsByQName[qname] = undefined;
      }

      var ns = attr.namespaceURI;
      // Now attr is the removed attribute.  Figure out its
      // ns+lname key and remove it from the other mapping as well.
      var key = (ns === null ? '' : ns) + '|' + attr.localName;
      this._attrsByLName[key] = undefined;

      var i = this._attrKeys.indexOf(key);
      if (this._attributes) {
        Array.prototype.splice.call(this._attributes, i, 1);
        this._attributes[qname] = undefined;
      }
      this._attrKeys.splice(i, 1);

      // Onchange handler for the attribute
      var onchange = attr.onchange;
      attr._setOwnerElement(null);
      if (onchange) {
        onchange.call(attr, this, attr.localName, attr.value, null);
      }
      // Mutation event
      if (this.rooted) this.ownerDocument.mutateRemoveAttr(attr);
    },
  },

  removeAttributeNS: {
    value: function removeAttributeNS(ns, lname) {
      ns = ns === undefined || ns === null ? '' : String(ns);
      lname = String(lname);
      var key = ns + '|' + lname;
      var attr = this._attrsByLName[key];
      if (!attr) return;

      this._attrsByLName[key] = undefined;

      var i = this._attrKeys.indexOf(key);
      if (this._attributes) {
        Array.prototype.splice.call(this._attributes, i, 1);
      }
      this._attrKeys.splice(i, 1);

      // Now find the same Attr object in the qname mapping and remove it
      // But be careful because there may be more than one match.
      this._removeQName(attr);

      // Onchange handler for the attribute
      var onchange = attr.onchange;
      attr._setOwnerElement(null);
      if (onchange) {
        onchange.call(attr, this, attr.localName, attr.value, null);
      }
      // Mutation event
      if (this.rooted) this.ownerDocument.mutateRemoveAttr(attr);
    },
  },

  removeAttributeNode: {
    value: function removeAttributeNode(attr) {
      var ns = attr.namespaceURI;
      var key = (ns === null ? '' : ns) + '|' + attr.localName;
      if (this._attrsByLName[key] !== attr) {
        utils.NotFoundError();
      }
      this.removeAttributeNS(ns, attr.localName);
      return attr;
    },
  },

  getAttributeNames: {
    value: function getAttributeNames() {
      var elt = this;
      return this._attrKeys.map(function (key) {
        return elt._attrsByLName[key].name;
      });
    },
  },

  // This 'raw' version of getAttribute is used by the getter functions
  // of reflected attributes. It skips some error checking and
  // namespace steps
  _getattr: {
    value: function _getattr(qname) {
      // Assume that qname is already lowercased, so don't do it here.
      // Also don't check whether attr is an array: a qname with no
      // prefix will never have two matching Attr objects (because
      // setAttributeNS doesn't allow a non-null namespace with a
      // null prefix.
      var attr = this._attrsByQName[qname];
      return attr ? attr.value : null;
    },
  },

  // The raw version of setAttribute for reflected idl attributes.
  _setattr: {
    value: function _setattr(qname, value) {
      var attr = this._attrsByQName[qname];
      var isnew;
      if (!attr) {
        attr = this._newattr(qname);
        isnew = true;
      }
      attr.value = String(value);
      if (this._attributes) this._attributes[qname] = attr;
      if (isnew && this._newattrhook) this._newattrhook(qname, value);
    },
  },

  // Create a new Attr object, insert it, and return it.
  // Used by setAttribute() and by set()
  _newattr: {
    value: function _newattr(qname) {
      var attr = new Attr(this, qname, null, null);
      var key = '|' + qname;
      this._attrsByQName[qname] = attr;
      this._attrsByLName[key] = attr;
      if (this._attributes) {
        this._attributes[this._attrKeys.length] = attr;
      }
      this._attrKeys.push(key);
      return attr;
    },
  },

  // Add a qname->Attr mapping to the _attrsByQName object, taking into
  // account that there may be more than one attr object with the
  // same qname
  _addQName: {
    value: function (attr) {
      var qname = attr.name;
      var existing = this._attrsByQName[qname];
      if (!existing) {
        this._attrsByQName[qname] = attr;
      } else if (Array.isArray(existing)) {
        existing.push(attr);
      } else {
        this._attrsByQName[qname] = [existing, attr];
      }
      if (this._attributes) this._attributes[qname] = attr;
    },
  },

  // Remove a qname->Attr mapping to the _attrsByQName object, taking into
  // account that there may be more than one attr object with the
  // same qname
  _removeQName: {
    value: function (attr) {
      var qname = attr.name;
      var target = this._attrsByQName[qname];

      if (Array.isArray(target)) {
        var idx = target.indexOf(attr);
        utils.assert(idx !== -1); // It must be here somewhere
        if (target.length === 2) {
          this._attrsByQName[qname] = target[1 - idx];
          if (this._attributes) {
            this._attributes[qname] = this._attrsByQName[qname];
          }
        } else {
          target.splice(idx, 1);
          if (this._attributes && this._attributes[qname] === attr) {
            this._attributes[qname] = target[0];
          }
        }
      } else {
        utils.assert(target === attr); // If only one, it must match
        this._attrsByQName[qname] = undefined;
        if (this._attributes) {
          this._attributes[qname] = undefined;
        }
      }
    },
  },

  // Return the number of attributes
  _numattrs: {
    get: function () {
      return this._attrKeys.length;
    },
  },
  // Return the nth Attr object
  _attr: {
    value: function (n) {
      return this._attrsByLName[this._attrKeys[n]];
    },
  },

  // Define getters and setters for an 'id' property that reflects
  // the content attribute 'id'.
  id: attributes.property({ name: 'id' }),

  // Define getters and setters for a 'className' property that reflects
  // the content attribute 'class'.
  className: attributes.property({ name: 'class' }),

  classList: {
    get: function () {
      var self = this;
      if (this._classList) {
        return this._classList;
      }
      var dtlist = new DOMTokenList(
        function () {
          return self.className || '';
        },
        function (v) {
          self.className = v;
        }
      );
      this._classList = dtlist;
      return dtlist;
    },
    set: function (v) {
      this.className = v;
    },
  },

  matches: {
    value: function (selector) {
      return select.matches(this, selector);
    },
  },

  closest: {
    value: function (selector) {
      var el = this;
      do {
        if (el.matches && el.matches(selector)) {
          return el;
        }
        el = el.parentElement || el.parentNode;
      } while (el !== null && el.nodeType === Node.ELEMENT_NODE);
      return null;
    },
  },

  querySelector: {
    value: function (selector) {
      return select(selector, this)[0];
    },
  },

  querySelectorAll: {
    value: function (selector) {
      var nodes = select(selector, this);
      return nodes.item ? nodes : new NodeList(nodes);
    },
  },
});

Object.defineProperties(Element.prototype, ChildNode);
Object.defineProperties(Element.prototype, NonDocumentTypeChildNode);

// Register special handling for the id attribute
attributes.registerChangeHandler(Element, 'id', function (element, lname, oldval, newval) {
  if (element.rooted) {
    if (oldval) {
      element.ownerDocument.delId(oldval, element);
    }
    if (newval) {
      element.ownerDocument.addId(newval, element);
    }
  }
});
attributes.registerChangeHandler(Element, 'class', function (element, lname, oldval, newval) {
  if (element._classList) {
    element._classList._update();
  }
});

// The Attr class represents a single attribute.  The values in
// _attrsByQName and _attrsByLName are instances of this class.
function Attr(elt, lname, prefix, namespace, value) {
  // localName and namespace are constant for any attr object.
  // But value may change.  And so can prefix, and so, therefore can name.
  this.localName = lname;
  this.prefix = prefix === null || prefix === '' ? null : '' + prefix;
  this.namespaceURI = namespace === null || namespace === '' ? null : '' + namespace;
  this.data = value;
  // Set ownerElement last to ensure it is hooked up to onchange handler
  this._setOwnerElement(elt);
}

// In DOM 3 Attr was supposed to extend Node; in DOM 4 that was abandoned.
Attr.prototype = Object.create(Object.prototype, {
  ownerElement: {
    get: function () {
      return this._ownerElement;
    },
  },
  _setOwnerElement: {
    value: function _setOwnerElement(elt) {
      this._ownerElement = elt;
      if (this.prefix === null && this.namespaceURI === null && elt) {
        this.onchange = elt._attributeChangeHandlers[this.localName];
      } else {
        this.onchange = null;
      }
    },
  },

  name: {
    get: function () {
      return this.prefix ? this.prefix + ':' + this.localName : this.localName;
    },
  },

  specified: {
    get: function () {
      // Deprecated
      return true;
    },
  },

  value: {
    get: function () {
      return this.data;
    },
    set: function (value) {
      var oldval = this.data;
      value = value === undefined ? '' : value + '';
      if (value === oldval) return;

      this.data = value;

      // Run the onchange hook for the attribute
      // if there is one.
      if (this.ownerElement) {
        if (this.onchange) this.onchange(this.ownerElement, this.localName, oldval, value);

        // Generate a mutation event if the element is rooted
        if (this.ownerElement.rooted) this.ownerElement.ownerDocument.mutateAttr(this, oldval);
      }
    },
  },

  cloneNode: {
    value: function cloneNode(deep) {
      // Both this method and Document#createAttribute*() create unowned Attrs
      return new Attr(null, this.localName, this.prefix, this.namespaceURI, this.data);
    },
  },

  // Legacy aliases (see gh#70 and https://dom.spec.whatwg.org/#interface-attr)
  nodeType: {
    get: function () {
      return Node.ATTRIBUTE_NODE;
    },
  },
  nodeName: {
    get: function () {
      return this.name;
    },
  },
  nodeValue: {
    get: function () {
      return this.value;
    },
    set: function (v) {
      this.value = v;
    },
  },
  textContent: {
    get: function () {
      return this.value;
    },
    set: function (v) {
      if (v === null || v === undefined) {
        v = '';
      }
      this.value = v;
    },
  },
});
// Sneakily export this class for use by Document.createAttribute()
Element._Attr = Attr;

// The attributes property of an Element will be an instance of this class.
// This class is really just a dummy, though. It only defines a length
// property and an item() method. The AttrArrayProxy that
// defines the public API just uses the Element object itself.
function AttributesArray(elt) {
  NamedNodeMap.call(this, elt);
  for (var name in elt._attrsByQName) {
    this[name] = elt._attrsByQName[name];
  }
  for (var i = 0; i < elt._attrKeys.length; i++) {
    this[i] = elt._attrsByLName[elt._attrKeys[i]];
  }
}
AttributesArray.prototype = Object.create(NamedNodeMap.prototype, {
  length: {
    get: function () {
      return this.element._attrKeys.length;
    },
    set: function () {
      /* ignore */
    },
  },
  item: {
    value: function (n) {
      /* jshint bitwise: false */
      n = n >>> 0;
      if (n >= this.length) {
        return null;
      }
      return this.element._attrsByLName[this.element._attrKeys[n]];
      /* jshint bitwise: true */
    },
  },
});

// We can't make direct array access work (without Proxies, node >=6)
// but we can make `Array.from(node.attributes)` and for-of loops work.
if (global.Symbol && global.Symbol.iterator) {
  AttributesArray.prototype[global.Symbol.iterator] = function () {
    var i = 0,
      n = this.length,
      self = this;
    return {
      next: function () {
        if (i < n) return { value: self.item(i++) };
        return { done: true };
      },
    };
  };
}

// The children property of an Element will be an instance of this class.
// It defines length, item() and namedItem() and will be wrapped by an
// HTMLCollection when exposed through the DOM.
function ChildrenCollection(e) {
  this.element = e;
  this.updateCache();
}

ChildrenCollection.prototype = Object.create(Object.prototype, {
  length: {
    get: function () {
      this.updateCache();
      return this.childrenByNumber.length;
    },
  },
  item: {
    value: function item(n) {
      this.updateCache();
      return this.childrenByNumber[n] || null;
    },
  },

  namedItem: {
    value: function namedItem(name) {
      this.updateCache();
      return this.childrenByName[name] || null;
    },
  },

  // This attribute returns the entire name->element map.
  // It is not part of the HTMLCollection API, but we need it in
  // src/HTMLCollectionProxy
  namedItems: {
    get: function () {
      this.updateCache();
      return this.childrenByName;
    },
  },

  updateCache: {
    value: function updateCache() {
      var namedElts = /^(a|applet|area|embed|form|frame|frameset|iframe|img|object)$/;
      if (this.lastModTime !== this.element.lastModTime) {
        this.lastModTime = this.element.lastModTime;

        var n = (this.childrenByNumber && this.childrenByNumber.length) || 0;
        for (var i = 0; i < n; i++) {
          this[i] = undefined;
        }

        this.childrenByNumber = [];
        this.childrenByName = Object.create(null);

        for (var c = this.element.firstChild; c !== null; c = c.nextSibling) {
          if (c.nodeType === Node.ELEMENT_NODE) {
            this[this.childrenByNumber.length] = c;
            this.childrenByNumber.push(c);

            // XXX Are there any requirements about the namespace
            // of the id property?
            var id = c.getAttribute('id');

            // If there is an id that is not already in use...
            if (id && !this.childrenByName[id]) this.childrenByName[id] = c;

            // For certain HTML elements we check the name attribute
            var name = c.getAttribute('name');
            if (
              name &&
              this.element.namespaceURI === NAMESPACE.HTML &&
              namedElts.test(this.element.localName) &&
              !this.childrenByName[name]
            )
              this.childrenByName[id] = c;
          }
        }
      }
    },
  },
});

// These functions return predicates for filtering elements.
// They're used by the Document and Element classes for methods like
// getElementsByTagName and getElementsByClassName

function localNameElementFilter(lname) {
  return function (e) {
    return e.localName === lname;
  };
}

function htmlLocalNameElementFilter(lname) {
  var lclname = utils.toASCIILowerCase(lname);
  if (lclname === lname) return localNameElementFilter(lname);

  return function (e) {
    return e.isHTML ? e.localName === lclname : e.localName === lname;
  };
}

function namespaceElementFilter(ns) {
  return function (e) {
    return e.namespaceURI === ns;
  };
}

function namespaceLocalNameElementFilter(ns, lname) {
  return function (e) {
    return e.namespaceURI === ns && e.localName === lname;
  };
}

function classNamesElementFilter(names) {
  return function (e) {
    return names.every(function (n) {
      return e.classList.contains(n);
    });
  };
}

function elementNameFilter(name) {
  return function (e) {
    // All the *HTML elements* in the document with the given name attribute
    if (e.namespaceURI !== NAMESPACE.HTML) {
      return false;
    }
    return e.getAttribute('name') === name;
  };
}
