'use strict';
var Node = require('./Node');
var Element = require('./Element');
var CSSStyleDeclaration = require('./CSSStyleDeclaration');
var utils = require('./utils');
var URLUtils = require('./URLUtils');
var defineElement = require('./defineElement');

var htmlElements = (exports.elements = {});
var htmlNameToImpl = Object.create(null);

exports.createElement = function (doc, localName, prefix) {
  var impl = htmlNameToImpl[localName] || HTMLUnknownElement;
  return new impl(doc, localName, prefix);
};

function define(spec) {
  return defineElement(spec, HTMLElement, htmlElements, htmlNameToImpl);
}

function URL(attr) {
  return {
    get: function () {
      var v = this._getattr(attr);
      if (v === null) {
        return '';
      }
      var url = this.doc._resolve(v);
      return url === null ? v : url;
    },
    set: function (value) {
      this._setattr(attr, value);
    },
  };
}

function CORS(attr) {
  return {
    get: function () {
      var v = this._getattr(attr);
      if (v === null) {
        return null;
      }
      if (v.toLowerCase() === 'use-credentials') {
        return 'use-credentials';
      }
      return 'anonymous';
    },
    set: function (value) {
      if (value === null || value === undefined) {
        this.removeAttribute(attr);
      } else {
        this._setattr(attr, value);
      }
    },
  };
}

var REFERRER = {
  type: [
    '',
    'no-referrer',
    'no-referrer-when-downgrade',
    'same-origin',
    'origin',
    'strict-origin',
    'origin-when-cross-origin',
    'strict-origin-when-cross-origin',
    'unsafe-url',
  ],
  missing: '',
};

// XXX: the default value for tabIndex should be 0 if the element is
// focusable and -1 if it is not.  But the full definition of focusable
// is actually hard to compute, so for now, I'll follow Firefox and
// just base the default value on the type of the element.
var focusableElements = {
  A: true,
  LINK: true,
  BUTTON: true,
  INPUT: true,
  SELECT: true,
  TEXTAREA: true,
  COMMAND: true,
};

var HTMLFormElement = function (doc, localName, prefix) {
  HTMLElement.call(this, doc, localName, prefix);
  this._form = null; // Prevent later deoptimization
};

var HTMLElement = (exports.HTMLElement = define({
  superclass: Element,
  name: 'HTMLElement',
  ctor: function HTMLElement(doc, localName, prefix) {
    Element.call(this, doc, localName, utils.NAMESPACE.HTML, prefix);
  },
  props: {
    dangerouslySetInnerHTML: {
      set: function (v) {
        this._innerHTML = v;
      },
    },
    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 instanceof htmlNameToImpl.template ? this.content : this;
        while (target.hasChildNodes()) target.removeChild(target.firstChild);

        // Now copy newly parsed children to this node
        target.appendChild(parser._asDocumentFragment());
      },
    },
    style: {
      get: function () {
        if (!this._style) this._style = new CSSStyleDeclaration(this);
        return this._style;
      },
      set: function (v) {
        if (v === null || v === undefined) {
          v = '';
        }
        this._setattr('style', String(v));
      },
    },

    // These can't really be implemented server-side in a reasonable way.
    blur: { value: function () {} },
    focus: { value: function () {} },
    forceSpellCheck: { value: function () {} },

    click: {
      value: function () {
        if (this._click_in_progress) return;
        this._click_in_progress = true;
        try {
          if (this._pre_click_activation_steps) this._pre_click_activation_steps();

          var event = this.ownerDocument.createEvent('MouseEvent');
          event.initMouseEvent(
            'click',
            true,
            true,
            this.ownerDocument.defaultView,
            1,
            0,
            0,
            0,
            0,
            // These 4 should be initialized with
            // the actually current keyboard state
            // somehow...
            false,
            false,
            false,
            false,
            0,
            null
          );

          // Dispatch this as an untrusted event since it is synthetic
          var success = this.dispatchEvent(event);

          if (success) {
            if (this._post_click_activation_steps) this._post_click_activation_steps(event);
          } else {
            if (this._cancelled_activation_steps) this._cancelled_activation_steps();
          }
        } finally {
          this._click_in_progress = false;
        }
      },
    },
    submit: { value: utils.nyi },
  },
  attributes: {
    title: String,
    lang: String,
    dir: { type: ['ltr', 'rtl', 'auto'], missing: '' },
    accessKey: String,
    hidden: Boolean,
    tabIndex: {
      type: 'long',
      default: function () {
        if (this.tagName in focusableElements || this.contentEditable) return 0;
        else return -1;
      },
    },
  },
  events: [
    'abort',
    'canplay',
    'canplaythrough',
    'change',
    'click',
    'contextmenu',
    'cuechange',
    'dblclick',
    'drag',
    'dragend',
    'dragenter',
    'dragleave',
    'dragover',
    'dragstart',
    'drop',
    'durationchange',
    'emptied',
    'ended',
    'input',
    'invalid',
    'keydown',
    'keypress',
    'keyup',
    'loadeddata',
    'loadedmetadata',
    'loadstart',
    'mousedown',
    'mousemove',
    'mouseout',
    'mouseover',
    'mouseup',
    'mousewheel',
    'pause',
    'play',
    'playing',
    'progress',
    'ratechange',
    'readystatechange',
    'reset',
    'seeked',
    'seeking',
    'select',
    'show',
    'stalled',
    'submit',
    'suspend',
    'timeupdate',
    'volumechange',
    'waiting',

    // These last 5 event types will be overriden by HTMLBodyElement
    'blur',
    'error',
    'focus',
    'load',
    'scroll',
  ],
}));

// XXX: reflect contextmenu as contextMenu, with element type

// style: the spec doesn't call this a reflected attribute.
//   may want to handle it manually.

// contentEditable: enumerated, not clear if it is actually
// reflected or requires custom getter/setter. Not listed as
// "limited to known values".  Raises syntax_err on bad setting,
// so I think this is custom.

// contextmenu: content is element id, idl type is an element
// draggable: boolean, but not a reflected attribute
// dropzone: reflected SettableTokenList, experimental, so don't
//   implement it right away.

// data-* attributes: need special handling in setAttribute?
// Or maybe that isn't necessary. Can I just scan the attribute list
// when building the dataset?  Liveness and caching issues?

// microdata attributes: many are simple reflected attributes, but
// I'm not going to implement this now.

const HTMLUnknownElement = define({
  name: 'HTMLUnknownElement',
  ctor: function HTMLUnknownElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
});

var formAssociatedProps = {
  // See http://www.w3.org/TR/html5/association-of-controls-and-forms.html#form-owner
  form: {
    get: function () {
      return this._form;
    },
  },
};

define({
  tag: 'a',
  name: 'HTMLAnchorElement',
  ctor: function HTMLAnchorElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  props: {
    _post_click_activation_steps: {
      value: function (e) {
        if (this.href) {
          // Follow the link
          // XXX: this is just a quick hack
          // XXX: the HTML spec probably requires more than this
          this.ownerDocument.defaultView.location = this.href;
        }
      },
    },
  },
  attributes: {
    href: URL,
    ping: String,
    download: String,
    target: String,
    rel: String,
    media: String,
    hreflang: String,
    type: String,
    referrerPolicy: REFERRER,
    // Obsolete
    coords: String,
    charset: String,
    name: String,
    rev: String,
    shape: String,
  },
});
// Latest WhatWG spec says these methods come via HTMLHyperlinkElementUtils
URLUtils._inherit(htmlNameToImpl.a.prototype);

define({
  tag: 'area',
  name: 'HTMLAreaElement',
  ctor: function HTMLAreaElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    alt: String,
    target: String,
    download: String,
    rel: String,
    media: String,
    href: URL,
    hreflang: String,
    type: String,
    shape: String,
    coords: String,
    ping: String,
    // XXX: also reflect relList
    referrerPolicy: REFERRER,
    // Obsolete
    noHref: Boolean,
  },
});
// Latest WhatWG spec says these methods come via HTMLHyperlinkElementUtils
URLUtils._inherit(htmlNameToImpl.area.prototype);

define({
  tag: 'br',
  name: 'HTMLBRElement',
  ctor: function HTMLBRElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    // Obsolete
    clear: String,
  },
});

define({
  tag: 'base',
  name: 'HTMLBaseElement',
  ctor: function HTMLBaseElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    target: String,
  },
});

define({
  tag: 'body',
  name: 'HTMLBodyElement',
  ctor: function HTMLBodyElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  // Certain event handler attributes on a <body> tag actually set
  // handlers for the window rather than just that element.  Define
  // getters and setters for those here.  Note that some of these override
  // properties on HTMLElement.prototype.
  // XXX: If I add support for <frameset>, these have to go there, too
  // XXX
  // When the Window object is implemented, these attribute will have
  // to work with the same-named attributes on the Window.
  events: [
    'afterprint',
    'beforeprint',
    'beforeunload',
    'blur',
    'error',
    'focus',
    'hashchange',
    'load',
    'message',
    'offline',
    'online',
    'pagehide',
    'pageshow',
    'popstate',
    'resize',
    'scroll',
    'storage',
    'unload',
  ],
  attributes: {
    // Obsolete
    text: { type: String, treatNullAsEmptyString: true },
    link: { type: String, treatNullAsEmptyString: true },
    vLink: { type: String, treatNullAsEmptyString: true },
    aLink: { type: String, treatNullAsEmptyString: true },
    bgColor: { type: String, treatNullAsEmptyString: true },
    background: String,
  },
});

define({
  tag: 'button',
  name: 'HTMLButtonElement',
  ctor: function HTMLButtonElement(doc, localName, prefix) {
    HTMLFormElement.call(this, doc, localName, prefix);
  },
  props: formAssociatedProps,
  attributes: {
    name: String,
    value: String,
    disabled: Boolean,
    autofocus: Boolean,
    type: { type: ['submit', 'reset', 'button', 'menu'], missing: 'submit' },
    formTarget: String,
    formNoValidate: Boolean,
    formMethod: { type: ['get', 'post', 'dialog'], invalid: 'get', missing: '' },
    formEnctype: {
      type: ['application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain'],
      invalid: 'application/x-www-form-urlencoded',
      missing: '',
    },
  },
});

define({
  tag: 'dl',
  name: 'HTMLDListElement',
  ctor: function HTMLDListElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    // Obsolete
    compact: Boolean,
  },
});

define({
  tag: 'data',
  name: 'HTMLDataElement',
  ctor: function HTMLDataElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    value: String,
  },
});

define({
  tag: 'datalist',
  name: 'HTMLDataListElement',
  ctor: function HTMLDataListElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
});

define({
  tag: 'details',
  name: 'HTMLDetailsElement',
  ctor: function HTMLDetailsElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    open: Boolean,
  },
});

define({
  tag: 'div',
  name: 'HTMLDivElement',
  ctor: function HTMLDivElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    // Obsolete
    align: String,
  },
});

define({
  tag: 'embed',
  name: 'HTMLEmbedElement',
  ctor: function HTMLEmbedElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    src: URL,
    type: String,
    width: String,
    height: String,
    // Obsolete
    align: String,
    name: String,
  },
});

define({
  tag: 'fieldset',
  name: 'HTMLFieldSetElement',
  ctor: function HTMLFieldSetElement(doc, localName, prefix) {
    HTMLFormElement.call(this, doc, localName, prefix);
  },
  props: formAssociatedProps,
  attributes: {
    disabled: Boolean,
    name: String,
  },
});

define({
  tag: 'form',
  name: 'HTMLFormElement',
  ctor: function HTMLFormElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    action: String,
    autocomplete: { type: ['on', 'off'], missing: 'on' },
    name: String,
    acceptCharset: { name: 'accept-charset' },
    target: String,
    noValidate: Boolean,
    method: { type: ['get', 'post', 'dialog'], invalid: 'get', missing: 'get' },
    // Both enctype and encoding reflect the enctype content attribute
    enctype: {
      type: ['application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain'],
      invalid: 'application/x-www-form-urlencoded',
      missing: 'application/x-www-form-urlencoded',
    },
    encoding: {
      name: 'enctype',
      type: ['application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain'],
      invalid: 'application/x-www-form-urlencoded',
      missing: 'application/x-www-form-urlencoded',
    },
  },
});

define({
  tag: 'hr',
  name: 'HTMLHRElement',
  ctor: function HTMLHRElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    // Obsolete
    align: String,
    color: String,
    noShade: Boolean,
    size: String,
    width: String,
  },
});

define({
  tag: 'head',
  name: 'HTMLHeadElement',
  ctor: function HTMLHeadElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
});

define({
  tags: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
  name: 'HTMLHeadingElement',
  ctor: function HTMLHeadingElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    // Obsolete
    align: String,
  },
});

define({
  tag: 'html',
  name: 'HTMLHtmlElement',
  ctor: function HTMLHtmlElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    // Obsolete
    version: String,
  },
});

define({
  tag: 'iframe',
  name: 'HTMLIFrameElement',
  ctor: function HTMLIFrameElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    src: URL,
    srcdoc: String,
    name: String,
    width: String,
    height: String,
    // XXX: sandbox is a reflected settable token list
    seamless: Boolean,
    allowFullscreen: Boolean,
    allowUserMedia: Boolean,
    allowPaymentRequest: Boolean,
    referrerPolicy: REFERRER,
    // Obsolete
    align: String,
    scrolling: String,
    frameBorder: String,
    longDesc: URL,
    marginHeight: { type: String, treatNullAsEmptyString: true },
    marginWidth: { type: String, treatNullAsEmptyString: true },
  },
});

define({
  tag: 'img',
  name: 'HTMLImageElement',
  ctor: function HTMLImageElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    alt: String,
    src: URL,
    srcset: String,
    crossOrigin: CORS,
    useMap: String,
    isMap: Boolean,
    height: { type: 'unsigned long', default: 0 },
    width: { type: 'unsigned long', default: 0 },
    referrerPolicy: REFERRER,
    // Obsolete:
    name: String,
    lowsrc: URL,
    align: String,
    hspace: { type: 'unsigned long', default: 0 },
    vspace: { type: 'unsigned long', default: 0 },
    longDesc: URL,
    border: { type: String, treatNullAsEmptyString: true },
  },
});

define({
  tag: 'input',
  name: 'HTMLInputElement',
  ctor: function HTMLInputElement(doc, localName, prefix) {
    HTMLFormElement.call(this, doc, localName, prefix);
  },
  props: {
    form: formAssociatedProps.form,
    _post_click_activation_steps: {
      value: function (e) {
        if (this.type === 'checkbox') {
          this.checked = !this.checked;
        } else if (this.type === 'radio') {
          var group = this.form.getElementsByName(this.name);
          for (var i = group.length - 1; i >= 0; i--) {
            var el = group[i];
            el.checked = el === this;
          }
        }
      },
    },
  },
  attributes: {
    name: String,
    disabled: Boolean,
    autofocus: Boolean,
    accept: String,
    alt: String,
    max: String,
    min: String,
    pattern: String,
    placeholder: String,
    step: String,
    dirName: String,
    defaultValue: { name: 'value' },
    multiple: Boolean,
    required: Boolean,
    readOnly: Boolean,
    checked: Boolean,
    value: String,
    src: URL,
    defaultChecked: { name: 'checked', type: Boolean },
    size: { type: 'unsigned long', default: 20, min: 1, setmin: 1 },
    width: { type: 'unsigned long', min: 0, setmin: 0, default: 0 },
    height: { type: 'unsigned long', min: 0, setmin: 0, default: 0 },
    minLength: { type: 'unsigned long', min: 0, setmin: 0, default: -1 },
    maxLength: { type: 'unsigned long', min: 0, setmin: 0, default: -1 },
    autocomplete: String, // It's complicated
    type: {
      type: [
        'text',
        'hidden',
        'search',
        'tel',
        'url',
        'email',
        'password',
        'datetime',
        'date',
        'month',
        'week',
        'time',
        'datetime-local',
        'number',
        'range',
        'color',
        'checkbox',
        'radio',
        'file',
        'submit',
        'image',
        'reset',
        'button',
      ],
      missing: 'text',
    },
    formTarget: String,
    formNoValidate: Boolean,
    formMethod: { type: ['get', 'post'], invalid: 'get', missing: '' },
    formEnctype: {
      type: ['application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain'],
      invalid: 'application/x-www-form-urlencoded',
      missing: '',
    },
    inputMode: {
      type: [
        'verbatim',
        'latin',
        'latin-name',
        'latin-prose',
        'full-width-latin',
        'kana',
        'kana-name',
        'katakana',
        'numeric',
        'tel',
        'email',
        'url',
      ],
      missing: '',
    },
    // Obsolete
    align: String,
    useMap: String,
  },
});

define({
  tag: 'keygen',
  name: 'HTMLKeygenElement',
  ctor: function HTMLKeygenElement(doc, localName, prefix) {
    HTMLFormElement.call(this, doc, localName, prefix);
  },
  props: formAssociatedProps,
  attributes: {
    name: String,
    disabled: Boolean,
    autofocus: Boolean,
    challenge: String,
    keytype: { type: ['rsa'], missing: '' },
  },
});

define({
  tag: 'li',
  name: 'HTMLLIElement',
  ctor: function HTMLLIElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    value: { type: 'long', default: 0 },
    // Obsolete
    type: String,
  },
});

define({
  tag: 'label',
  name: 'HTMLLabelElement',
  ctor: function HTMLLabelElement(doc, localName, prefix) {
    HTMLFormElement.call(this, doc, localName, prefix);
  },
  props: formAssociatedProps,
  attributes: {
    htmlFor: { name: 'for', type: String },
  },
});

define({
  tag: 'legend',
  name: 'HTMLLegendElement',
  ctor: function HTMLLegendElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    // Obsolete
    align: String,
  },
});

define({
  tag: 'link',
  name: 'HTMLLinkElement',
  ctor: function HTMLLinkElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    // XXX Reflect DOMSettableTokenList sizes also DOMTokenList relList
    href: URL,
    rel: String,
    media: String,
    hreflang: String,
    type: String,
    crossOrigin: CORS,
    nonce: String,
    integrity: String,
    referrerPolicy: REFERRER,
    // Obsolete
    charset: String,
    rev: String,
    target: String,
  },
});

define({
  tag: 'map',
  name: 'HTMLMapElement',
  ctor: function HTMLMapElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    name: String,
  },
});

define({
  tag: 'menu',
  name: 'HTMLMenuElement',
  ctor: function HTMLMenuElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    // XXX: not quite right, default should be popup if parent element is
    // popup.
    type: { type: ['context', 'popup', 'toolbar'], missing: 'toolbar' },
    label: String,
    // Obsolete
    compact: Boolean,
  },
});

define({
  tag: 'meta',
  name: 'HTMLMetaElement',
  ctor: function HTMLMetaElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    name: String,
    content: String,
    httpEquiv: { name: 'http-equiv', type: String },
    // Obsolete
    scheme: String,
  },
});

define({
  tag: 'meter',
  name: 'HTMLMeterElement',
  ctor: function HTMLMeterElement(doc, localName, prefix) {
    HTMLFormElement.call(this, doc, localName, prefix);
  },
  props: formAssociatedProps,
});

define({
  tags: ['ins', 'del'],
  name: 'HTMLModElement',
  ctor: function HTMLModElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    cite: URL,
    dateTime: String,
  },
});

define({
  tag: 'ol',
  name: 'HTMLOListElement',
  ctor: function HTMLOListElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  props: {
    // Utility function (see the start attribute default value). Returns
    // the number of <li> children of this element
    _numitems: {
      get: function () {
        var items = 0;
        this.childNodes.forEach(function (n) {
          if (n.nodeType === Node.ELEMENT_NODE && n.tagName === 'LI') items++;
        });
        return items;
      },
    },
  },
  attributes: {
    type: String,
    reversed: Boolean,
    start: {
      type: 'long',
      default: function () {
        // The default value of the start attribute is 1 unless the list is
        // reversed. Then it is the # of li children
        if (this.reversed) return this._numitems;
        else return 1;
      },
    },
    // Obsolete
    compact: Boolean,
  },
});

define({
  tag: 'object',
  name: 'HTMLObjectElement',
  ctor: function HTMLObjectElement(doc, localName, prefix) {
    HTMLFormElement.call(this, doc, localName, prefix);
  },
  props: formAssociatedProps,
  attributes: {
    data: URL,
    type: String,
    name: String,
    useMap: String,
    typeMustMatch: Boolean,
    width: String,
    height: String,
    // Obsolete
    align: String,
    archive: String,
    code: String,
    declare: Boolean,
    hspace: { type: 'unsigned long', default: 0 },
    standby: String,
    vspace: { type: 'unsigned long', default: 0 },
    codeBase: URL,
    codeType: String,
    border: { type: String, treatNullAsEmptyString: true },
  },
});

define({
  tag: 'optgroup',
  name: 'HTMLOptGroupElement',
  ctor: function HTMLOptGroupElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    disabled: Boolean,
    label: String,
  },
});

define({
  tag: 'option',
  name: 'HTMLOptionElement',
  ctor: function HTMLOptionElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  props: {
    form: {
      get: function () {
        var p = this.parentNode;
        while (p && p.nodeType === Node.ELEMENT_NODE) {
          if (p.localName === 'select') return p.form;
          p = p.parentNode;
        }
      },
    },
    value: {
      get: function () {
        return this._getattr('value') || this.text;
      },
      set: function (v) {
        this._setattr('value', v);
      },
    },
    text: {
      get: function () {
        // Strip and collapse whitespace
        return this.textContent.replace(/[ \t\n\f\r]+/g, ' ').trim();
      },
      set: function (v) {
        this.textContent = v;
      },
    },
    // missing: index
  },
  attributes: {
    disabled: Boolean,
    defaultSelected: { name: 'selected', type: Boolean },
    label: String,
  },
});

define({
  tag: 'output',
  name: 'HTMLOutputElement',
  ctor: function HTMLOutputElement(doc, localName, prefix) {
    HTMLFormElement.call(this, doc, localName, prefix);
  },
  props: formAssociatedProps,
  attributes: {
    // XXX Reflect for/htmlFor as a settable token list
    name: String,
  },
});

define({
  tag: 'p',
  name: 'HTMLParagraphElement',
  ctor: function HTMLParagraphElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    // Obsolete
    align: String,
  },
});

define({
  tag: 'param',
  name: 'HTMLParamElement',
  ctor: function HTMLParamElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    name: String,
    value: String,
    // Obsolete
    type: String,
    valueType: String,
  },
});

define({
  tags: ['pre', /*legacy elements:*/ 'listing', 'xmp'],
  name: 'HTMLPreElement',
  ctor: function HTMLPreElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    // Obsolete
    width: { type: 'long', default: 0 },
  },
});

define({
  tag: 'progress',
  name: 'HTMLProgressElement',
  ctor: function HTMLProgressElement(doc, localName, prefix) {
    HTMLFormElement.call(this, doc, localName, prefix);
  },
  props: formAssociatedProps,
  attributes: {
    max: { type: Number, float: true, default: 1.0, min: 0 },
  },
});

define({
  tags: ['q', 'blockquote'],
  name: 'HTMLQuoteElement',
  ctor: function HTMLQuoteElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    cite: URL,
  },
});

define({
  tag: 'script',
  name: 'HTMLScriptElement',
  ctor: function HTMLScriptElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  props: {
    text: {
      get: function () {
        var s = '';
        for (var i = 0, n = this.childNodes.length; i < n; i++) {
          var child = this.childNodes[i];
          if (child.nodeType === Node.TEXT_NODE) s += child._data;
        }
        return s;
      },
      set: function (value) {
        this.removeChildren();
        if (value !== null && value !== '') {
          this.appendChild(this.ownerDocument.createTextNode(value));
        }
      },
    },
  },
  attributes: {
    src: URL,
    type: String,
    charset: String,
    defer: Boolean,
    async: Boolean,
    crossOrigin: CORS,
    nonce: String,
    integrity: String,
  },
});

define({
  tag: 'select',
  name: 'HTMLSelectElement',
  ctor: function HTMLSelectElement(doc, localName, prefix) {
    HTMLFormElement.call(this, doc, localName, prefix);
  },
  props: {
    form: formAssociatedProps.form,
    options: {
      get: function () {
        return this.getElementsByTagName('option');
      },
    },
  },
  attributes: {
    autocomplete: String, // It's complicated
    name: String,
    disabled: Boolean,
    autofocus: Boolean,
    multiple: Boolean,
    required: Boolean,
    size: { type: 'unsigned long', default: 0 },
  },
});

define({
  tag: 'source',
  name: 'HTMLSourceElement',
  ctor: function HTMLSourceElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    src: URL,
    type: String,
    media: String,
  },
});

define({
  tag: 'span',
  name: 'HTMLSpanElement',
  ctor: function HTMLSpanElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
});

define({
  tag: 'style',
  name: 'HTMLStyleElement',
  ctor: function HTMLStyleElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    media: String,
    type: String,
    scoped: Boolean,
  },
});

define({
  tag: 'caption',
  name: 'HTMLTableCaptionElement',
  ctor: function HTMLTableCaptionElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    // Obsolete
    align: String,
  },
});

define({
  name: 'HTMLTableCellElement',
  ctor: function HTMLTableCellElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    colSpan: { type: 'unsigned long', default: 1 },
    rowSpan: { type: 'unsigned long', default: 1 },
    //XXX Also reflect settable token list headers
    scope: { type: ['row', 'col', 'rowgroup', 'colgroup'], missing: '' },
    abbr: String,
    // Obsolete
    align: String,
    axis: String,
    height: String,
    width: String,
    ch: { name: 'char', type: String },
    chOff: { name: 'charoff', type: String },
    noWrap: Boolean,
    vAlign: String,
    bgColor: { type: String, treatNullAsEmptyString: true },
  },
});

define({
  tags: ['col', 'colgroup'],
  name: 'HTMLTableColElement',
  ctor: function HTMLTableColElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    span: { type: 'limited unsigned long with fallback', default: 1, min: 1 },
    // Obsolete
    align: String,
    ch: { name: 'char', type: String },
    chOff: { name: 'charoff', type: String },
    vAlign: String,
    width: String,
  },
});

define({
  tag: 'table',
  name: 'HTMLTableElement',
  ctor: function HTMLTableElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  props: {
    rows: {
      get: function () {
        return this.getElementsByTagName('tr');
      },
    },
  },
  attributes: {
    // Obsolete
    align: String,
    border: String,
    frame: String,
    rules: String,
    summary: String,
    width: String,
    bgColor: { type: String, treatNullAsEmptyString: true },
    cellPadding: { type: String, treatNullAsEmptyString: true },
    cellSpacing: { type: String, treatNullAsEmptyString: true },
  },
});

define({
  tag: 'template',
  name: 'HTMLTemplateElement',
  ctor: function HTMLTemplateElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
    this._contentFragment = doc._templateDoc.createDocumentFragment();
  },
  props: {
    content: {
      get: function () {
        return this._contentFragment;
      },
    },
    serialize: {
      value: function () {
        return this.content.serialize();
      },
    },
  },
});

define({
  tag: 'tr',
  name: 'HTMLTableRowElement',
  ctor: function HTMLTableRowElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  props: {
    cells: {
      get: function () {
        return this.querySelectorAll('td,th');
      },
    },
  },
  attributes: {
    // Obsolete
    align: String,
    ch: { name: 'char', type: String },
    chOff: { name: 'charoff', type: String },
    vAlign: String,
    bgColor: { type: String, treatNullAsEmptyString: true },
  },
});

define({
  tags: ['thead', 'tfoot', 'tbody'],
  name: 'HTMLTableSectionElement',
  ctor: function HTMLTableSectionElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  props: {
    rows: {
      get: function () {
        return this.getElementsByTagName('tr');
      },
    },
  },
  attributes: {
    // Obsolete
    align: String,
    ch: { name: 'char', type: String },
    chOff: { name: 'charoff', type: String },
    vAlign: String,
  },
});

define({
  tag: 'textarea',
  name: 'HTMLTextAreaElement',
  ctor: function HTMLTextAreaElement(doc, localName, prefix) {
    HTMLFormElement.call(this, doc, localName, prefix);
  },
  props: {
    form: formAssociatedProps.form,
    type: {
      get: function () {
        return 'textarea';
      },
    },
    defaultValue: {
      get: function () {
        return this.textContent;
      },
      set: function (v) {
        this.textContent = v;
      },
    },
    value: {
      get: function () {
        return this.defaultValue; /* never dirty */
      },
      set: function (v) {
        // This isn't completely correct: according to the spec, this
        // should "dirty" the API value, and result in
        // `this.value !== this.defaultValue`.  But for most of what
        // folks want to do, this implementation should be fine:
        this.defaultValue = v;
      },
    },
    textLength: {
      get: function () {
        return this.value.length;
      },
    },
  },
  attributes: {
    autocomplete: String, // It's complicated
    name: String,
    disabled: Boolean,
    autofocus: Boolean,
    placeholder: String,
    wrap: String,
    dirName: String,
    required: Boolean,
    readOnly: Boolean,
    rows: { type: 'limited unsigned long with fallback', default: 2 },
    cols: { type: 'limited unsigned long with fallback', default: 20 },
    maxLength: { type: 'unsigned long', min: 0, setmin: 0, default: -1 },
    minLength: { type: 'unsigned long', min: 0, setmin: 0, default: -1 },
    inputMode: {
      type: [
        'verbatim',
        'latin',
        'latin-name',
        'latin-prose',
        'full-width-latin',
        'kana',
        'kana-name',
        'katakana',
        'numeric',
        'tel',
        'email',
        'url',
      ],
      missing: '',
    },
  },
});

define({
  tag: 'time',
  name: 'HTMLTimeElement',
  ctor: function HTMLTimeElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    dateTime: String,
    pubDate: Boolean,
  },
});

define({
  tag: 'title',
  name: 'HTMLTitleElement',
  ctor: function HTMLTitleElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  props: {
    text: {
      get: function () {
        return this.textContent;
      },
    },
  },
});

define({
  tag: 'ul',
  name: 'HTMLUListElement',
  ctor: function HTMLUListElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    type: String,
    // Obsolete
    compact: Boolean,
  },
});

define({
  name: 'HTMLMediaElement',
  ctor: function HTMLMediaElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    src: URL,
    crossOrigin: CORS,
    preload: { type: ['metadata', 'none', 'auto', { value: '', alias: 'auto' }], missing: 'auto' },
    loop: Boolean,
    autoplay: Boolean,
    mediaGroup: String,
    controls: Boolean,
    defaultMuted: { name: 'muted', type: Boolean },
  },
});

define({
  tag: 'audio',
  superclass: htmlElements.HTMLMediaElement,
  name: 'HTMLAudioElement',
  ctor: function HTMLAudioElement(doc, localName, prefix) {
    htmlElements.HTMLMediaElement.call(this, doc, localName, prefix);
  },
});

define({
  tag: 'video',
  superclass: htmlElements.HTMLMediaElement,
  name: 'HTMLVideoElement',
  ctor: function HTMLVideoElement(doc, localName, prefix) {
    htmlElements.HTMLMediaElement.call(this, doc, localName, prefix);
  },
  attributes: {
    poster: URL,
    width: { type: 'unsigned long', min: 0, default: 0 },
    height: { type: 'unsigned long', min: 0, default: 0 },
  },
});

define({
  tag: 'td',
  superclass: htmlElements.HTMLTableCellElement,
  name: 'HTMLTableDataCellElement',
  ctor: function HTMLTableDataCellElement(doc, localName, prefix) {
    htmlElements.HTMLTableCellElement.call(this, doc, localName, prefix);
  },
});

define({
  tag: 'th',
  superclass: htmlElements.HTMLTableCellElement,
  name: 'HTMLTableHeaderCellElement',
  ctor: function HTMLTableHeaderCellElement(doc, localName, prefix) {
    htmlElements.HTMLTableCellElement.call(this, doc, localName, prefix);
  },
});

define({
  tag: 'frameset',
  name: 'HTMLFrameSetElement',
  ctor: function HTMLFrameSetElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
});

define({
  tag: 'frame',
  name: 'HTMLFrameElement',
  ctor: function HTMLFrameElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
});

define({
  tag: 'canvas',
  name: 'HTMLCanvasElement',
  ctor: function HTMLCanvasElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  props: {
    getContext: { value: utils.nyi },
    probablySupportsContext: { value: utils.nyi },
    setContext: { value: utils.nyi },
    transferControlToProxy: { value: utils.nyi },
    toDataURL: { value: utils.nyi },
    toBlob: { value: utils.nyi },
  },
  attributes: {
    width: { type: 'unsigned long', default: 300 },
    height: { type: 'unsigned long', default: 150 },
  },
});

define({
  tag: 'dialog',
  name: 'HTMLDialogElement',
  ctor: function HTMLDialogElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  props: {
    show: { value: utils.nyi },
    showModal: { value: utils.nyi },
    close: { value: utils.nyi },
  },
  attributes: {
    open: Boolean,
    returnValue: String,
  },
});

define({
  tag: 'menuitem',
  name: 'HTMLMenuItemElement',
  ctor: function HTMLMenuItemElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  props: {
    // The menuitem's label
    _label: {
      get: function () {
        var val = this._getattr('label');
        if (val !== null && val !== '') {
          return val;
        }
        val = this.textContent;
        // Strip and collapse whitespace
        return val.replace(/[ \t\n\f\r]+/g, ' ').trim();
      },
    },
    // The menuitem label IDL attribute
    label: {
      get: function () {
        var val = this._getattr('label');
        if (val !== null) {
          return val;
        }
        return this._label;
      },
      set: function (v) {
        this._setattr('label', v);
      },
    },
  },
  attributes: {
    type: { type: ['command', 'checkbox', 'radio'], missing: 'command' },
    icon: URL,
    disabled: Boolean,
    checked: Boolean,
    radiogroup: String,
    default: Boolean,
  },
});

define({
  tag: 'source',
  name: 'HTMLSourceElement',
  ctor: function HTMLSourceElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    srcset: String,
    sizes: String,
    media: String,
    src: URL,
    type: String,
  },
});

define({
  tag: 'track',
  name: 'HTMLTrackElement',
  ctor: function HTMLTrackElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    src: URL,
    srclang: String,
    label: String,
    default: Boolean,
    kind: {
      type: ['subtitles', 'captions', 'descriptions', 'chapters', 'metadata'],
      missing: 'subtitles',
      invalid: 'metadata',
    },
  },
  props: {
    NONE: {
      get: function () {
        return 0;
      },
    },
    LOADING: {
      get: function () {
        return 1;
      },
    },
    LOADED: {
      get: function () {
        return 2;
      },
    },
    ERROR: {
      get: function () {
        return 3;
      },
    },
    readyState: { get: utils.nyi },
    track: { get: utils.nyi },
  },
});

define({
  // obsolete
  tag: 'font',
  name: 'HTMLFontElement',
  ctor: function HTMLFontElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    color: { type: String, treatNullAsEmptyString: true },
    face: { type: String },
    size: { type: String },
  },
});

define({
  // obsolete
  tag: 'dir',
  name: 'HTMLDirectoryElement',
  ctor: function HTMLDirectoryElement(doc, localName, prefix) {
    HTMLElement.call(this, doc, localName, prefix);
  },
  attributes: {
    compact: Boolean,
  },
});

define({
  tags: [
    'abbr',
    'address',
    'article',
    'aside',
    'b',
    'bdi',
    'bdo',
    'cite',
    'code',
    'dd',
    'dfn',
    'dt',
    'em',
    'figcaption',
    'figure',
    'footer',
    'header',
    'hgroup',
    'i',
    'kbd',
    'main',
    'mark',
    'nav',
    'noscript',
    'rb',
    'rp',
    'rt',
    'rtc',
    'ruby',
    's',
    'samp',
    'section',
    'small',
    'strong',
    'sub',
    'summary',
    'sup',
    'u',
    'var',
    'wbr',
    // Legacy elements
    'acronym',
    'basefont',
    'big',
    'center',
    'nobr',
    'noembed',
    'noframes',
    'plaintext',
    'strike',
    'tt',
  ],
});
