/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import type {HostContext, HostContextDev} from './ReactFiberConfigDOM';

import {HostContextNamespaceNone} from './ReactFiberConfigDOM';

import {
  registrationNameDependencies,
  possibleRegistrationNames,
} from '../events/EventRegistry';

import {checkHtmlStringCoercion} from 'shared/CheckStringCoercion';
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';

import {
  getValueForAttribute,
  getValueForAttributeOnCustomComponent,
  setValueForPropertyOnCustomComponent,
  setValueForKnownAttribute,
  setValueForAttribute,
  setValueForNamespacedAttribute,
} from './DOMPropertyOperations';
import {
  validateInputProps,
  initInput,
  updateInput,
  restoreControlledInputState,
} from './ReactDOMInput';
import {validateOptionProps} from './ReactDOMOption';
import {
  validateSelectProps,
  initSelect,
  restoreControlledSelectState,
  updateSelect,
} from './ReactDOMSelect';
import {
  validateTextareaProps,
  initTextarea,
  updateTextarea,
  restoreControlledTextareaState,
} from './ReactDOMTextarea';
import {setSrcObject} from './ReactDOMSrcObject';
import {validateTextNesting} from './validateDOMNesting';
import setTextContent from './setTextContent';
import {
  createDangerousStringForStyles,
  setValueForStyles,
} from './CSSPropertyOperations';
import {SVG_NAMESPACE, MATH_NAMESPACE} from './DOMNamespaces';
import isCustomElement from '../shared/isCustomElement';
import getAttributeAlias from '../shared/getAttributeAlias';
import possibleStandardNames from '../shared/possibleStandardNames';
import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook';
import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';
import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
import sanitizeURL from '../shared/sanitizeURL';

import noop from 'shared/noop';

import {trackHostMutation} from 'react-reconciler/src/ReactFiberMutationTracking';

import {
  enableHydrationChangeEvent,
  enableScrollEndPolyfill,
  enableSrcObject,
  enableTrustedTypesIntegration,
  enableViewTransition,
} from 'shared/ReactFeatureFlags';
import {
  mediaEventTypes,
  listenToNonDelegatedEvent,
} from '../events/DOMPluginEventSystem';

let didWarnControlledToUncontrolled = false;
let didWarnUncontrolledToControlled = false;
let didWarnFormActionType = false;
let didWarnFormActionName = false;
let didWarnFormActionTarget = false;
let didWarnFormActionMethod = false;
let didWarnForNewBooleanPropsWithEmptyValue: {[string]: boolean};
let didWarnPopoverTargetObject = false;
if (__DEV__) {
  didWarnForNewBooleanPropsWithEmptyValue = {};
}

function validatePropertiesInDevelopment(type: string, props: any) {
  if (__DEV__) {
    validateARIAProperties(type, props);
    validateInputProperties(type, props);
    validateUnknownProperties(type, props, {
      registrationNameDependencies,
      possibleRegistrationNames,
    });
    if (
      props.contentEditable &&
      !props.suppressContentEditableWarning &&
      props.children != null
    ) {
      console.error(
        'A component is `contentEditable` and contains `children` managed by ' +
          'React. It is now your responsibility to guarantee that none of ' +
          'those nodes are unexpectedly modified or duplicated. This is ' +
          'probably not intentional.',
      );
    }
  }
}

function validateFormActionInDevelopment(
  tag: string,
  key: string,
  value: mixed,
  props: any,
) {
  if (__DEV__) {
    if (value == null) {
      return;
    }
    if (tag === 'form') {
      if (key === 'formAction') {
        console.error(
          'You can only pass the formAction prop to <input> or <button>. Use the action prop on <form>.',
        );
      } else if (typeof value === 'function') {
        if (
          (props.encType != null || props.method != null) &&
          !didWarnFormActionMethod
        ) {
          didWarnFormActionMethod = true;
          console.error(
            'Cannot specify a encType or method for a form that specifies a ' +
              'function as the action. React provides those automatically. ' +
              'They will get overridden.',
          );
        }
        if (props.target != null && !didWarnFormActionTarget) {
          didWarnFormActionTarget = true;
          console.error(
            'Cannot specify a target for a form that specifies a function as the action. ' +
              'The function will always be executed in the same window.',
          );
        }
      }
    } else if (tag === 'input' || tag === 'button') {
      if (key === 'action') {
        console.error(
          'You can only pass the action prop to <form>. Use the formAction prop on <input> or <button>.',
        );
      } else if (
        tag === 'input' &&
        props.type !== 'submit' &&
        props.type !== 'image' &&
        !didWarnFormActionType
      ) {
        didWarnFormActionType = true;
        console.error(
          'An input can only specify a formAction along with type="submit" or type="image".',
        );
      } else if (
        tag === 'button' &&
        props.type != null &&
        props.type !== 'submit' &&
        !didWarnFormActionType
      ) {
        didWarnFormActionType = true;
        console.error(
          'A button can only specify a formAction along with type="submit" or no type.',
        );
      } else if (typeof value === 'function') {
        // Function form actions cannot control the form properties
        if (props.name != null && !didWarnFormActionName) {
          didWarnFormActionName = true;
          console.error(
            'Cannot specify a "name" prop for a button that specifies a function as a formAction. ' +
              'React needs it to encode which action should be invoked. It will get overridden.',
          );
        }
        if (
          (props.formEncType != null || props.formMethod != null) &&
          !didWarnFormActionMethod
        ) {
          didWarnFormActionMethod = true;
          console.error(
            'Cannot specify a formEncType or formMethod for a button that specifies a ' +
              'function as a formAction. React provides those automatically. They will get overridden.',
          );
        }
        if (props.formTarget != null && !didWarnFormActionTarget) {
          didWarnFormActionTarget = true;
          console.error(
            'Cannot specify a formTarget for a button that specifies a function as a formAction. ' +
              'The function will always be executed in the same window.',
          );
        }
      }
    } else {
      if (key === 'action') {
        console.error('You can only pass the action prop to <form>.');
      } else {
        console.error(
          'You can only pass the formAction prop to <input> or <button>.',
        );
      }
    }
  }
}

function warnForPropDifference(
  propName: string,
  serverValue: mixed,
  clientValue: mixed,
  serverDifferences: {[propName: string]: mixed},
): void {
  if (__DEV__) {
    if (serverValue === clientValue) {
      return;
    }
    const normalizedClientValue =
      normalizeMarkupForTextOrAttribute(clientValue);
    const normalizedServerValue =
      normalizeMarkupForTextOrAttribute(serverValue);
    if (normalizedServerValue === normalizedClientValue) {
      return;
    }

    serverDifferences[propName] = serverValue;
  }
}

function warnForExtraAttributes(
  domElement: Element,
  attributeNames: Set<string>,
  serverDifferences: {[propName: string]: mixed},
) {
  if (__DEV__) {
    attributeNames.forEach(function (attributeName) {
      serverDifferences[getPropNameFromAttributeName(attributeName)] =
        attributeName === 'style'
          ? getStylesObjectFromElement(domElement)
          : domElement.getAttribute(attributeName);
    });
  }
}

function warnForInvalidEventListener(registrationName: string, listener: any) {
  if (__DEV__) {
    if (listener === false) {
      console.error(
        'Expected `%s` listener to be a function, instead got `false`.\n\n' +
          'If you used to conditionally omit it with %s={condition && value}, ' +
          'pass %s={condition ? value : undefined} instead.',
        registrationName,
        registrationName,
        registrationName,
      );
    } else {
      console.error(
        'Expected `%s` listener to be a function, instead got a value of `%s` type.',
        registrationName,
        typeof listener,
      );
    }
  }
}

// Parse the HTML and read it back to normalize the HTML string so that it
// can be used for comparison.
function normalizeHTML(parent: Element, html: string) {
  if (__DEV__) {
    // We could have created a separate document here to avoid
    // re-initializing custom elements if they exist. But this breaks
    // how <noscript> is being handled. So we use the same document.
    // See the discussion in https://github.com/facebook/react/pull/11157.
    const testElement =
      parent.namespaceURI === MATH_NAMESPACE ||
      parent.namespaceURI === SVG_NAMESPACE
        ? parent.ownerDocument.createElementNS(
            (parent.namespaceURI: any),
            parent.tagName,
          )
        : parent.ownerDocument.createElement(parent.tagName);
    testElement.innerHTML = html;
    return testElement.innerHTML;
  }
}

// HTML parsing normalizes CR and CRLF to LF.
// It also can turn \u0000 into \uFFFD inside attributes.
// https://www.w3.org/TR/html5/single-page.html#preprocessing-the-input-stream
// If we have a mismatch, it might be caused by that.
// We will still patch up in this case but not fire the warning.
const NORMALIZE_NEWLINES_REGEX = /\r\n?/g;
const NORMALIZE_NULL_AND_REPLACEMENT_REGEX = /\u0000|\uFFFD/g;

function normalizeMarkupForTextOrAttribute(markup: mixed): string {
  if (__DEV__) {
    checkHtmlStringCoercion(markup);
  }
  const markupString = typeof markup === 'string' ? markup : '' + (markup: any);
  return markupString
    .replace(NORMALIZE_NEWLINES_REGEX, '\n')
    .replace(NORMALIZE_NULL_AND_REPLACEMENT_REGEX, '');
}

function checkForUnmatchedText(
  serverText: string,
  clientText: string | number | bigint,
) {
  const normalizedClientText = normalizeMarkupForTextOrAttribute(clientText);
  const normalizedServerText = normalizeMarkupForTextOrAttribute(serverText);
  if (normalizedServerText === normalizedClientText) {
    return true;
  }
  return false;
}

export function trapClickOnNonInteractiveElement(node: HTMLElement) {
  // Mobile Safari does not fire properly bubble click events on
  // non-interactive elements, which means delegated click listeners do not
  // fire. The workaround for this bug involves attaching an empty click
  // listener on the target node.
  // https://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
  // Just set it using the onclick property so that we don't have to manage any
  // bookkeeping for it. Not sure if we need to clear it when the listener is
  // removed.
  // TODO: Only do this for the relevant Safaris maybe?
  node.onclick = noop;
}

const xlinkNamespace = 'http://www.w3.org/1999/xlink';
const xmlNamespace = 'http://www.w3.org/XML/1998/namespace';

function setProp(
  domElement: Element,
  tag: string,
  key: string,
  value: mixed,
  props: any,
  prevValue: mixed,
): void {
  switch (key) {
    case 'children': {
      if (typeof value === 'string') {
        if (__DEV__) {
          validateTextNesting(value, tag, false);
        }
        // Avoid setting initial textContent when the text is empty. In IE11 setting
        // textContent on a <textarea> will cause the placeholder to not
        // show within the <textarea> until it has been focused and blurred again.
        // https://github.com/facebook/react/issues/6731#issuecomment-254874553
        const canSetTextContent =
          tag !== 'body' && (tag !== 'textarea' || value !== '');
        if (canSetTextContent) {
          setTextContent(domElement, value);
        }
      } else if (typeof value === 'number' || typeof value === 'bigint') {
        if (__DEV__) {
          // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
          validateTextNesting('' + value, tag, false);
        }
        const canSetTextContent = tag !== 'body';
        if (canSetTextContent) {
          // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
          setTextContent(domElement, '' + value);
        }
      } else {
        return;
      }
      break;
    }
    // These are very common props and therefore are in the beginning of the switch.
    // TODO: aria-label is a very common prop but allows booleans so is not like the others
    // but should ideally go in this list too.
    case 'className':
      setValueForKnownAttribute(domElement, 'class', value);
      break;
    case 'tabIndex':
      // This has to be case sensitive in SVG.
      setValueForKnownAttribute(domElement, 'tabindex', value);
      break;
    case 'dir':
    case 'role':
    case 'viewBox':
    case 'width':
    case 'height': {
      setValueForKnownAttribute(domElement, key, value);
      break;
    }
    case 'style': {
      setValueForStyles(domElement, value, prevValue);
      return;
    }
    // These attributes accept URLs. These must not allow javascript: URLS.
    case 'data':
      if (tag !== 'object') {
        setValueForKnownAttribute(domElement, 'data', value);
        break;
      }
    // fallthrough
    case 'src': {
      if (enableSrcObject && typeof value === 'object' && value !== null) {
        // Some tags support object sources like Blob, File, MediaSource and MediaStream.
        if (tag === 'img' || tag === 'video' || tag === 'audio') {
          try {
            setSrcObject(domElement, tag, value);
            break;
          } catch (x) {
            // If URL.createObjectURL() errors, it was probably some other object type
            // that should be toString:ed instead, so we just fall-through to the normal
            // path.
          }
        } else {
          if (__DEV__) {
            try {
              // This should always error.
              URL.revokeObjectURL(URL.createObjectURL((value: any)));
              if (tag === 'source') {
                console.error(
                  'Passing Blob, MediaSource or MediaStream to <source src> is not supported. ' +
                    'Pass it directly to <img src>, <video src> or <audio src> instead.',
                );
              } else {
                console.error(
                  'Passing Blob, MediaSource or MediaStream to <%s src> is not supported.',
                  tag,
                );
              }
            } catch (x) {}
          }
        }
      }
      // Fallthrough
    }
    case 'href': {
      if (
        value === '' &&
        // <a href=""> is fine for "reload" links.
        !(tag === 'a' && key === 'href')
      ) {
        if (__DEV__) {
          if (key === 'src') {
            console.error(
              'An empty string ("") was passed to the %s attribute. ' +
                'This may cause the browser to download the whole page again over the network. ' +
                'To fix this, either do not render the element at all ' +
                'or pass null to %s instead of an empty string.',
              key,
              key,
            );
          } else {
            console.error(
              'An empty string ("") was passed to the %s attribute. ' +
                'To fix this, either do not render the element at all ' +
                'or pass null to %s instead of an empty string.',
              key,
              key,
            );
          }
        }
        domElement.removeAttribute(key);
        break;
      }
      if (
        value == null ||
        typeof value === 'function' ||
        typeof value === 'symbol' ||
        typeof value === 'boolean'
      ) {
        domElement.removeAttribute(key);
        break;
      }
      // `setAttribute` with objects becomes only `[object]` in IE8/9,
      // ('' + value) makes it output the correct toString()-value.
      if (__DEV__) {
        checkAttributeStringCoercion(value, key);
      }
      const sanitizedValue = (sanitizeURL(
        enableTrustedTypesIntegration ? value : '' + (value: any),
      ): any);
      domElement.setAttribute(key, sanitizedValue);
      break;
    }
    case 'action':
    case 'formAction': {
      // TODO: Consider moving these special cases to the form, input and button tags.
      if (__DEV__) {
        validateFormActionInDevelopment(tag, key, value, props);
      }
      if (typeof value === 'function') {
        // Set a javascript URL that doesn't do anything. We don't expect this to be invoked
        // because we'll preventDefault, but it can happen if a form is manually submitted or
        // if someone calls stopPropagation before React gets the event.
        // If CSP is used to block javascript: URLs that's fine too. It just won't show this
        // error message but the URL will be logged.
        domElement.setAttribute(
          key,
          // eslint-disable-next-line no-script-url
          "javascript:throw new Error('" +
            'A React form was unexpectedly submitted. If you called form.submit() manually, ' +
            "consider using form.requestSubmit() instead. If you\\'re trying to use " +
            'event.stopPropagation() in a submit event handler, consider also calling ' +
            'event.preventDefault().' +
            "')",
        );
        break;
      } else if (typeof prevValue === 'function') {
        // When we're switching off a Server Action that was originally hydrated.
        // The server control these fields during SSR that are now trailing.
        // The regular diffing doesn't apply since we compare against the previous props.
        // Instead, we need to force them to be set to whatever they should be now.
        // This would be a lot cleaner if we did this whole fork in the per-tag approach.
        if (key === 'formAction') {
          if (tag !== 'input') {
            // Setting the name here isn't completely safe for inputs if this is switching
            // to become a radio button. In that case we let the tag based override take
            // control.
            setProp(domElement, tag, 'name', props.name, props, null);
          }
          setProp(
            domElement,
            tag,
            'formEncType',
            props.formEncType,
            props,
            null,
          );
          setProp(domElement, tag, 'formMethod', props.formMethod, props, null);
          setProp(domElement, tag, 'formTarget', props.formTarget, props, null);
        } else {
          setProp(domElement, tag, 'encType', props.encType, props, null);
          setProp(domElement, tag, 'method', props.method, props, null);
          setProp(domElement, tag, 'target', props.target, props, null);
        }
      }
      if (
        value == null ||
        typeof value === 'symbol' ||
        typeof value === 'boolean'
      ) {
        domElement.removeAttribute(key);
        break;
      }
      // `setAttribute` with objects becomes only `[object]` in IE8/9,
      // ('' + value) makes it output the correct toString()-value.
      if (__DEV__) {
        checkAttributeStringCoercion(value, key);
      }
      const sanitizedValue = (sanitizeURL(
        enableTrustedTypesIntegration ? value : '' + (value: any),
      ): any);
      domElement.setAttribute(key, sanitizedValue);
      break;
    }
    case 'onClick': {
      // TODO: This cast may not be sound for SVG, MathML or custom elements.
      if (value != null) {
        if (__DEV__ && typeof value !== 'function') {
          warnForInvalidEventListener(key, value);
        }
        trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
      }
      return;
    }
    case 'onScroll': {
      if (value != null) {
        if (__DEV__ && typeof value !== 'function') {
          warnForInvalidEventListener(key, value);
        }
        listenToNonDelegatedEvent('scroll', domElement);
      }
      return;
    }
    case 'onScrollEnd': {
      if (value != null) {
        if (__DEV__ && typeof value !== 'function') {
          warnForInvalidEventListener(key, value);
        }
        listenToNonDelegatedEvent('scrollend', domElement);
        if (enableScrollEndPolyfill) {
          // For use by the polyfill.
          listenToNonDelegatedEvent('scroll', domElement);
        }
      }
      return;
    }
    case 'dangerouslySetInnerHTML': {
      if (value != null) {
        if (typeof value !== 'object' || !('__html' in value)) {
          throw new Error(
            '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
              'Please visit https://react.dev/link/dangerously-set-inner-html ' +
              'for more information.',
          );
        }
        const nextHtml: any = value.__html;
        if (nextHtml != null) {
          if (props.children != null) {
            throw new Error(
              'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
            );
          }
          domElement.innerHTML = nextHtml;
        }
      }
      break;
    }
    // Note: `option.selected` is not updated if `select.multiple` is
    // disabled with `removeAttribute`. We have special logic for handling this.
    case 'multiple': {
      (domElement: any).multiple =
        value && typeof value !== 'function' && typeof value !== 'symbol';
      break;
    }
    case 'muted': {
      (domElement: any).muted =
        value && typeof value !== 'function' && typeof value !== 'symbol';
      break;
    }
    case 'suppressContentEditableWarning':
    case 'suppressHydrationWarning':
    case 'defaultValue': // Reserved
    case 'defaultChecked':
    case 'innerHTML':
    case 'ref': {
      // TODO: `ref` is pretty common, should we move it up?
      // Noop
      break;
    }
    case 'autoFocus': {
      // We polyfill it separately on the client during commit.
      // We could have excluded it in the property list instead of
      // adding a special case here, but then it wouldn't be emitted
      // on server rendering (but we *do* want to emit it in SSR).
      break;
    }
    case 'xlinkHref': {
      if (
        value == null ||
        typeof value === 'function' ||
        typeof value === 'boolean' ||
        typeof value === 'symbol'
      ) {
        domElement.removeAttribute('xlink:href');
        break;
      }
      // `setAttribute` with objects becomes only `[object]` in IE8/9,
      // ('' + value) makes it output the correct toString()-value.
      if (__DEV__) {
        checkAttributeStringCoercion(value, key);
      }
      const sanitizedValue = (sanitizeURL(
        enableTrustedTypesIntegration ? value : '' + (value: any),
      ): any);
      domElement.setAttributeNS(xlinkNamespace, 'xlink:href', sanitizedValue);
      break;
    }
    case 'contentEditable':
    case 'spellCheck':
    case 'draggable':
    case 'value':
    case 'autoReverse':
    case 'externalResourcesRequired':
    case 'focusable':
    case 'preserveAlpha': {
      // Booleanish String
      // These are "enumerated" attributes that accept "true" and "false".
      // In React, we let users pass `true` and `false` even though technically
      // these aren't boolean attributes (they are coerced to strings).
      // The SVG attributes are case-sensitive. Since the HTML attributes are
      // insensitive they also work even though we canonically use lower case.
      if (
        value != null &&
        typeof value !== 'function' &&
        typeof value !== 'symbol'
      ) {
        if (__DEV__) {
          checkAttributeStringCoercion(value, key);
        }
        domElement.setAttribute(
          key,
          enableTrustedTypesIntegration ? (value: any) : '' + (value: any),
        );
      } else {
        domElement.removeAttribute(key);
      }
      break;
    }
    // Boolean
    case 'inert': {
      if (__DEV__) {
        if (value === '' && !didWarnForNewBooleanPropsWithEmptyValue[key]) {
          didWarnForNewBooleanPropsWithEmptyValue[key] = true;
          console.error(
            'Received an empty string for a boolean attribute `%s`. ' +
              'This will treat the attribute as if it were false. ' +
              'Either pass `false` to silence this warning, or ' +
              'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.',
            key,
          );
        }
      }
    }
    // Fallthrough for boolean props that don't have a warning for empty strings.
    case 'allowFullScreen':
    case 'async':
    case 'autoPlay':
    case 'controls':
    case 'default':
    case 'defer':
    case 'disabled':
    case 'disablePictureInPicture':
    case 'disableRemotePlayback':
    case 'formNoValidate':
    case 'hidden':
    case 'loop':
    case 'noModule':
    case 'noValidate':
    case 'open':
    case 'playsInline':
    case 'readOnly':
    case 'required':
    case 'reversed':
    case 'scoped':
    case 'seamless':
    case 'itemScope': {
      if (value && typeof value !== 'function' && typeof value !== 'symbol') {
        domElement.setAttribute(key, '');
      } else {
        domElement.removeAttribute(key);
      }
      break;
    }
    // Overloaded Boolean
    case 'capture':
    case 'download': {
      // An attribute that can be used as a flag as well as with a value.
      // When true, it should be present (set either to an empty string or its name).
      // When false, it should be omitted.
      // For any other value, should be present with that value.
      if (value === true) {
        domElement.setAttribute(key, '');
      } else if (
        value !== false &&
        value != null &&
        typeof value !== 'function' &&
        typeof value !== 'symbol'
      ) {
        if (__DEV__) {
          checkAttributeStringCoercion(value, key);
        }
        domElement.setAttribute(key, (value: any));
      } else {
        domElement.removeAttribute(key);
      }
      break;
    }
    case 'cols':
    case 'rows':
    case 'size':
    case 'span': {
      // These are HTML attributes that must be positive numbers.
      if (
        value != null &&
        typeof value !== 'function' &&
        typeof value !== 'symbol' &&
        !isNaN(value) &&
        (value: any) >= 1
      ) {
        if (__DEV__) {
          checkAttributeStringCoercion(value, key);
        }
        domElement.setAttribute(key, (value: any));
      } else {
        domElement.removeAttribute(key);
      }
      break;
    }
    case 'rowSpan':
    case 'start': {
      // These are HTML attributes that must be numbers.
      if (
        value != null &&
        typeof value !== 'function' &&
        typeof value !== 'symbol' &&
        !isNaN(value)
      ) {
        if (__DEV__) {
          checkAttributeStringCoercion(value, key);
        }
        domElement.setAttribute(key, (value: any));
      } else {
        domElement.removeAttribute(key);
      }
      break;
    }
    case 'popover':
      listenToNonDelegatedEvent('beforetoggle', domElement);
      listenToNonDelegatedEvent('toggle', domElement);
      setValueForAttribute(domElement, 'popover', value);
      break;
    case 'xlinkActuate':
      setValueForNamespacedAttribute(
        domElement,
        xlinkNamespace,
        'xlink:actuate',
        value,
      );
      break;
    case 'xlinkArcrole':
      setValueForNamespacedAttribute(
        domElement,
        xlinkNamespace,
        'xlink:arcrole',
        value,
      );
      break;
    case 'xlinkRole':
      setValueForNamespacedAttribute(
        domElement,
        xlinkNamespace,
        'xlink:role',
        value,
      );
      break;
    case 'xlinkShow':
      setValueForNamespacedAttribute(
        domElement,
        xlinkNamespace,
        'xlink:show',
        value,
      );
      break;
    case 'xlinkTitle':
      setValueForNamespacedAttribute(
        domElement,
        xlinkNamespace,
        'xlink:title',
        value,
      );
      break;
    case 'xlinkType':
      setValueForNamespacedAttribute(
        domElement,
        xlinkNamespace,
        'xlink:type',
        value,
      );
      break;
    case 'xmlBase':
      setValueForNamespacedAttribute(
        domElement,
        xmlNamespace,
        'xml:base',
        value,
      );
      break;
    case 'xmlLang':
      setValueForNamespacedAttribute(
        domElement,
        xmlNamespace,
        'xml:lang',
        value,
      );
      break;
    case 'xmlSpace':
      setValueForNamespacedAttribute(
        domElement,
        xmlNamespace,
        'xml:space',
        value,
      );
      break;
    // Properties that should not be allowed on custom elements.
    case 'is': {
      if (__DEV__) {
        if (prevValue != null) {
          console.error(
            'Cannot update the "is" prop after it has been initialized.',
          );
        }
      }
      // TODO: We shouldn't actually set this attribute, because we've already
      // passed it to createElement. We don't also need the attribute.
      // However, our tests currently query for it so it's plausible someone
      // else does too so it's break.
      setValueForAttribute(domElement, 'is', value);
      break;
    }
    case 'innerText':
    case 'textContent':
      return;
    case 'popoverTarget':
      if (__DEV__) {
        if (
          !didWarnPopoverTargetObject &&
          value != null &&
          typeof value === 'object'
        ) {
          didWarnPopoverTargetObject = true;
          console.error(
            'The `popoverTarget` prop expects the ID of an Element as a string. Received %s instead.',
            value,
          );
        }
      }
    // Fall through
    default: {
      if (
        key.length > 2 &&
        (key[0] === 'o' || key[0] === 'O') &&
        (key[1] === 'n' || key[1] === 'N')
      ) {
        if (
          __DEV__ &&
          registrationNameDependencies.hasOwnProperty(key) &&
          value != null &&
          typeof value !== 'function'
        ) {
          warnForInvalidEventListener(key, value);
        }
        // Updating events doesn't affect the visuals.
        return;
      } else {
        const attributeName = getAttributeAlias(key);
        setValueForAttribute(domElement, attributeName, value);
      }
    }
  }
  // To avoid marking things as host mutations we do early returns above.
  trackHostMutation();
}

function setPropOnCustomElement(
  domElement: Element,
  tag: string,
  key: string,
  value: mixed,
  props: any,
  prevValue: mixed,
): void {
  switch (key) {
    case 'style': {
      setValueForStyles(domElement, value, prevValue);
      return;
    }
    case 'dangerouslySetInnerHTML': {
      if (value != null) {
        if (typeof value !== 'object' || !('__html' in value)) {
          throw new Error(
            '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
              'Please visit https://react.dev/link/dangerously-set-inner-html ' +
              'for more information.',
          );
        }
        const nextHtml: any = value.__html;
        if (nextHtml != null) {
          if (props.children != null) {
            throw new Error(
              'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
            );
          }
          domElement.innerHTML = nextHtml;
        }
      }
      break;
    }
    case 'children': {
      if (typeof value === 'string') {
        setTextContent(domElement, value);
      } else if (typeof value === 'number' || typeof value === 'bigint') {
        // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
        setTextContent(domElement, '' + value);
      } else {
        return;
      }
      break;
    }
    case 'onScroll': {
      if (value != null) {
        if (__DEV__ && typeof value !== 'function') {
          warnForInvalidEventListener(key, value);
        }
        listenToNonDelegatedEvent('scroll', domElement);
      }
      return;
    }
    case 'onScrollEnd': {
      if (value != null) {
        if (__DEV__ && typeof value !== 'function') {
          warnForInvalidEventListener(key, value);
        }
        listenToNonDelegatedEvent('scrollend', domElement);
        if (enableScrollEndPolyfill) {
          // For use by the polyfill.
          listenToNonDelegatedEvent('scroll', domElement);
        }
      }
      return;
    }
    case 'onClick': {
      // TODO: This cast may not be sound for SVG, MathML or custom elements.
      if (value != null) {
        if (__DEV__ && typeof value !== 'function') {
          warnForInvalidEventListener(key, value);
        }
        trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
      }
      return;
    }
    case 'suppressContentEditableWarning':
    case 'suppressHydrationWarning':
    case 'innerHTML':
    case 'ref': {
      // Noop
      return;
    }
    case 'innerText': // Properties
    case 'textContent':
      return;
    // Fall through
    default: {
      if (registrationNameDependencies.hasOwnProperty(key)) {
        if (__DEV__ && value != null && typeof value !== 'function') {
          warnForInvalidEventListener(key, value);
        }
        return;
      } else {
        setValueForPropertyOnCustomComponent(domElement, key, value);
        // We track mutations inside this call.
        return;
      }
    }
  }
  // To avoid marking things as host mutations we do early returns above.
  trackHostMutation();
}

export function setInitialProperties(
  domElement: Element,
  tag: string,
  props: Object,
): void {
  if (__DEV__) {
    validatePropertiesInDevelopment(tag, props);
  }

  // TODO: Make sure that we check isMounted before firing any of these events.

  switch (tag) {
    case 'div':
    case 'span':
    case 'svg':
    case 'path':
    case 'a':
    case 'g':
    case 'p':
    case 'li': {
      // Fast track the most common tag types
      break;
    }
    // img tags previously were implemented as void elements with non delegated events however Safari (and possibly Firefox)
    // begin fetching the image as soon as the `src` or `srcSet` property is set and if we set these before other properties
    // that can modify the request (such as crossorigin) or the resource fetch (such as sizes) then the browser will load
    // the wrong thing or load more than one thing. This implementation ensures src and srcSet are set on the instance last
    case 'img': {
      listenToNonDelegatedEvent('error', domElement);
      listenToNonDelegatedEvent('load', domElement);
      // Mostly a port of Void Element logic with special casing to ensure srcset and src are set last
      let hasSrc = false;
      let hasSrcSet = false;
      for (const propKey in props) {
        if (!props.hasOwnProperty(propKey)) {
          continue;
        }
        const propValue = props[propKey];
        if (propValue == null) {
          continue;
        }
        switch (propKey) {
          case 'src':
            hasSrc = true;
            break;
          case 'srcSet':
            hasSrcSet = true;
            break;
          case 'children':
          case 'dangerouslySetInnerHTML': {
            // TODO: Can we make this a DEV warning to avoid this deny list?
            throw new Error(
              `${tag} is a void element tag and must neither have \`children\` nor ` +
                'use `dangerouslySetInnerHTML`.',
            );
          }
          // defaultChecked and defaultValue are ignored by setProp
          default: {
            setProp(domElement, tag, propKey, propValue, props, null);
          }
        }
      }
      if (hasSrcSet) {
        setProp(domElement, tag, 'srcSet', props.srcSet, props, null);
      }
      if (hasSrc) {
        setProp(domElement, tag, 'src', props.src, props, null);
      }
      return;
    }
    case 'input': {
      if (__DEV__) {
        checkControlledValueProps('input', props);
      }
      // We listen to this event in case to ensure emulated bubble
      // listeners still fire for the invalid event.
      listenToNonDelegatedEvent('invalid', domElement);

      let name = null;
      let type = null;
      let value = null;
      let defaultValue = null;
      let checked = null;
      let defaultChecked = null;
      for (const propKey in props) {
        if (!props.hasOwnProperty(propKey)) {
          continue;
        }
        const propValue = props[propKey];
        if (propValue == null) {
          continue;
        }
        switch (propKey) {
          case 'name': {
            name = propValue;
            break;
          }
          case 'type': {
            type = propValue;
            break;
          }
          case 'checked': {
            checked = propValue;
            break;
          }
          case 'defaultChecked': {
            defaultChecked = propValue;
            break;
          }
          case 'value': {
            value = propValue;
            break;
          }
          case 'defaultValue': {
            defaultValue = propValue;
            break;
          }
          case 'children':
          case 'dangerouslySetInnerHTML': {
            if (propValue != null) {
              throw new Error(
                `${tag} is a void element tag and must neither have \`children\` nor ` +
                  'use `dangerouslySetInnerHTML`.',
              );
            }
            break;
          }
          default: {
            setProp(domElement, tag, propKey, propValue, props, null);
          }
        }
      }
      // TODO: Make sure we check if this is still unmounted or do any clean
      // up necessary since we never stop tracking anymore.
      validateInputProps(domElement, props);
      initInput(
        domElement,
        value,
        defaultValue,
        checked,
        defaultChecked,
        type,
        name,
        false,
      );
      return;
    }
    case 'select': {
      if (__DEV__) {
        checkControlledValueProps('select', props);
      }
      // We listen to this event in case to ensure emulated bubble
      // listeners still fire for the invalid event.
      listenToNonDelegatedEvent('invalid', domElement);
      let value = null;
      let defaultValue = null;
      let multiple = null;
      for (const propKey in props) {
        if (!props.hasOwnProperty(propKey)) {
          continue;
        }
        const propValue = props[propKey];
        if (propValue == null) {
          continue;
        }
        switch (propKey) {
          case 'value': {
            value = propValue;
            // This is handled by initSelect below.
            break;
          }
          case 'defaultValue': {
            defaultValue = propValue;
            // This is handled by initSelect below.
            break;
          }
          case 'multiple': {
            multiple = propValue;
            // TODO: We don't actually have to fall through here because we set it
            // in initSelect anyway. We can remove the special case in setProp.
          }
          // Fallthrough
          default: {
            setProp(domElement, tag, propKey, propValue, props, null);
          }
        }
      }
      validateSelectProps(domElement, props);
      initSelect(domElement, value, defaultValue, multiple);
      return;
    }
    case 'textarea': {
      if (__DEV__) {
        checkControlledValueProps('textarea', props);
      }
      // We listen to this event in case to ensure emulated bubble
      // listeners still fire for the invalid event.
      listenToNonDelegatedEvent('invalid', domElement);
      let value = null;
      let defaultValue = null;
      let children = null;
      for (const propKey in props) {
        if (!props.hasOwnProperty(propKey)) {
          continue;
        }
        const propValue = props[propKey];
        if (propValue == null) {
          continue;
        }
        switch (propKey) {
          case 'value': {
            value = propValue;
            // This is handled by initTextarea below.
            break;
          }
          case 'defaultValue': {
            defaultValue = propValue;
            break;
          }
          case 'children': {
            children = propValue;
            // Handled by initTextarea above.
            break;
          }
          case 'dangerouslySetInnerHTML': {
            if (propValue != null) {
              // TODO: Do we really need a special error message for this. It's also pretty blunt.
              throw new Error(
                '`dangerouslySetInnerHTML` does not make sense on <textarea>.',
              );
            }
            break;
          }
          default: {
            setProp(domElement, tag, propKey, propValue, props, null);
          }
        }
      }
      // TODO: Make sure we check if this is still unmounted or do any clean
      // up necessary since we never stop tracking anymore.
      validateTextareaProps(domElement, props);
      initTextarea(domElement, value, defaultValue, children);
      return;
    }
    case 'option': {
      validateOptionProps(domElement, props);
      for (const propKey in props) {
        if (!props.hasOwnProperty(propKey)) {
          continue;
        }
        const propValue = props[propKey];
        if (propValue == null) {
          continue;
        }
        switch (propKey) {
          case 'selected': {
            // TODO: Remove support for selected on option.
            (domElement: any).selected =
              propValue &&
              typeof propValue !== 'function' &&
              typeof propValue !== 'symbol';
            break;
          }
          default: {
            setProp(domElement, tag, propKey, propValue, props, null);
          }
        }
      }
      return;
    }
    case 'dialog': {
      listenToNonDelegatedEvent('beforetoggle', domElement);
      listenToNonDelegatedEvent('toggle', domElement);
      listenToNonDelegatedEvent('cancel', domElement);
      listenToNonDelegatedEvent('close', domElement);
      break;
    }
    case 'iframe':
    case 'object': {
      // We listen to this event in case to ensure emulated bubble
      // listeners still fire for the load event.
      listenToNonDelegatedEvent('load', domElement);
      break;
    }
    case 'video':
    case 'audio': {
      // We listen to these events in case to ensure emulated bubble
      // listeners still fire for all the media events.
      for (let i = 0; i < mediaEventTypes.length; i++) {
        listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
      }
      break;
    }
    case 'image': {
      // We listen to these events in case to ensure emulated bubble
      // listeners still fire for error and load events.
      listenToNonDelegatedEvent('error', domElement);
      listenToNonDelegatedEvent('load', domElement);
      break;
    }
    case 'details': {
      // We listen to this event in case to ensure emulated bubble
      // listeners still fire for the toggle event.
      listenToNonDelegatedEvent('toggle', domElement);
      break;
    }
    case 'embed':
    case 'source':
    case 'link': {
      // These are void elements that also need delegated events.
      listenToNonDelegatedEvent('error', domElement);
      listenToNonDelegatedEvent('load', domElement);
      // We fallthrough to the return of the void elements
    }
    case 'area':
    case 'base':
    case 'br':
    case 'col':
    case 'hr':
    case 'keygen':
    case 'meta':
    case 'param':
    case 'track':
    case 'wbr':
    case 'menuitem': {
      // Void elements
      for (const propKey in props) {
        if (!props.hasOwnProperty(propKey)) {
          continue;
        }
        const propValue = props[propKey];
        if (propValue == null) {
          continue;
        }
        switch (propKey) {
          case 'children':
          case 'dangerouslySetInnerHTML': {
            // TODO: Can we make this a DEV warning to avoid this deny list?
            throw new Error(
              `${tag} is a void element tag and must neither have \`children\` nor ` +
                'use `dangerouslySetInnerHTML`.',
            );
          }
          // defaultChecked and defaultValue are ignored by setProp
          default: {
            setProp(domElement, tag, propKey, propValue, props, null);
          }
        }
      }
      return;
    }
    default: {
      if (isCustomElement(tag, props)) {
        for (const propKey in props) {
          if (!props.hasOwnProperty(propKey)) {
            continue;
          }
          const propValue = props[propKey];
          if (propValue === undefined) {
            continue;
          }
          setPropOnCustomElement(
            domElement,
            tag,
            propKey,
            propValue,
            props,
            undefined,
          );
        }
        return;
      }
    }
  }

  for (const propKey in props) {
    if (!props.hasOwnProperty(propKey)) {
      continue;
    }
    const propValue = props[propKey];
    if (propValue == null) {
      continue;
    }
    setProp(domElement, tag, propKey, propValue, props, null);
  }
}

export function updateProperties(
  domElement: Element,
  tag: string,
  lastProps: Object,
  nextProps: Object,
): void {
  if (__DEV__) {
    validatePropertiesInDevelopment(tag, nextProps);
  }

  switch (tag) {
    case 'div':
    case 'span':
    case 'svg':
    case 'path':
    case 'a':
    case 'g':
    case 'p':
    case 'li': {
      // Fast track the most common tag types
      break;
    }
    case 'input': {
      let name = null;
      let type = null;
      let value = null;
      let defaultValue = null;
      let lastDefaultValue = null;
      let checked = null;
      let defaultChecked = null;
      for (const propKey in lastProps) {
        const lastProp = lastProps[propKey];
        if (lastProps.hasOwnProperty(propKey) && lastProp != null) {
          switch (propKey) {
            case 'checked': {
              break;
            }
            case 'value': {
              // This is handled by updateWrapper below.
              break;
            }
            case 'defaultValue': {
              lastDefaultValue = lastProp;
            }
            // defaultChecked and defaultValue are ignored by setProp
            // Fallthrough
            default: {
              if (!nextProps.hasOwnProperty(propKey))
                setProp(domElement, tag, propKey, null, nextProps, lastProp);
            }
          }
        }
      }
      for (const propKey in nextProps) {
        const nextProp = nextProps[propKey];
        const lastProp = lastProps[propKey];
        if (
          nextProps.hasOwnProperty(propKey) &&
          (nextProp != null || lastProp != null)
        ) {
          switch (propKey) {
            case 'type': {
              if (nextProp !== lastProp) {
                trackHostMutation();
              }
              type = nextProp;
              break;
            }
            case 'name': {
              if (nextProp !== lastProp) {
                trackHostMutation();
              }
              name = nextProp;
              break;
            }
            case 'checked': {
              if (nextProp !== lastProp) {
                trackHostMutation();
              }
              checked = nextProp;
              break;
            }
            case 'defaultChecked': {
              if (nextProp !== lastProp) {
                trackHostMutation();
              }
              defaultChecked = nextProp;
              break;
            }
            case 'value': {
              if (nextProp !== lastProp) {
                trackHostMutation();
              }
              value = nextProp;
              break;
            }
            case 'defaultValue': {
              if (nextProp !== lastProp) {
                trackHostMutation();
              }
              defaultValue = nextProp;
              break;
            }
            case 'children':
            case 'dangerouslySetInnerHTML': {
              if (nextProp != null) {
                throw new Error(
                  `${tag} is a void element tag and must neither have \`children\` nor ` +
                    'use `dangerouslySetInnerHTML`.',
                );
              }
              break;
            }
            default: {
              if (nextProp !== lastProp)
                setProp(
                  domElement,
                  tag,
                  propKey,
                  nextProp,
                  nextProps,
                  lastProp,
                );
            }
          }
        }
      }

      if (__DEV__) {
        const wasControlled =
          lastProps.type === 'checkbox' || lastProps.type === 'radio'
            ? lastProps.checked != null
            : lastProps.value != null;
        const isControlled =
          nextProps.type === 'checkbox' || nextProps.type === 'radio'
            ? nextProps.checked != null
            : nextProps.value != null;

        if (
          !wasControlled &&
          isControlled &&
          !didWarnUncontrolledToControlled
        ) {
          console.error(
            'A component is changing an uncontrolled input to be controlled. ' +
              'This is likely caused by the value changing from undefined to ' +
              'a defined value, which should not happen. ' +
              'Decide between using a controlled or uncontrolled input ' +
              'element for the lifetime of the component. More info: https://react.dev/link/controlled-components',
          );
          didWarnUncontrolledToControlled = true;
        }
        if (
          wasControlled &&
          !isControlled &&
          !didWarnControlledToUncontrolled
        ) {
          console.error(
            'A component is changing a controlled input to be uncontrolled. ' +
              'This is likely caused by the value changing from a defined to ' +
              'undefined, which should not happen. ' +
              'Decide between using a controlled or uncontrolled input ' +
              'element for the lifetime of the component. More info: https://react.dev/link/controlled-components',
          );
          didWarnControlledToUncontrolled = true;
        }
      }

      // Update the wrapper around inputs *after* updating props. This has to
      // happen after updating the rest of props. Otherwise HTML5 input validations
      // raise warnings and prevent the new value from being assigned.
      updateInput(
        domElement,
        value,
        defaultValue,
        lastDefaultValue,
        checked,
        defaultChecked,
        type,
        name,
      );
      return;
    }
    case 'select': {
      let value = null;
      let defaultValue = null;
      let multiple = null;
      let wasMultiple = null;
      for (const propKey in lastProps) {
        const lastProp = lastProps[propKey];
        if (lastProps.hasOwnProperty(propKey) && lastProp != null) {
          switch (propKey) {
            case 'value': {
              // This is handled by updateWrapper below.
              break;
            }
            // defaultValue are ignored by setProp
            case 'multiple': {
              wasMultiple = lastProp;
              // TODO: Move special case in here from setProp.
            }
            // Fallthrough
            default: {
              if (!nextProps.hasOwnProperty(propKey)) {
                setProp(domElement, tag, propKey, null, nextProps, lastProp);
              }
            }
          }
        }
      }
      for (const propKey in nextProps) {
        const nextProp = nextProps[propKey];
        const lastProp = lastProps[propKey];
        if (
          nextProps.hasOwnProperty(propKey) &&
          (nextProp != null || lastProp != null)
        ) {
          switch (propKey) {
            case 'value': {
              if (nextProp !== lastProp) {
                trackHostMutation();
              }
              value = nextProp;
              // This is handled by updateSelect below.
              break;
            }
            case 'defaultValue': {
              if (nextProp !== lastProp) {
                trackHostMutation();
              }
              defaultValue = nextProp;
              break;
            }
            case 'multiple': {
              if (nextProp !== lastProp) {
                trackHostMutation();
              }
              multiple = nextProp;
              // TODO: Just move the special case in here from setProp.
            }
            // Fallthrough
            default: {
              if (nextProp !== lastProp)
                setProp(
                  domElement,
                  tag,
                  propKey,
                  nextProp,
                  nextProps,
                  lastProp,
                );
            }
          }
        }
      }
      // <select> value update needs to occur after <option> children
      // reconciliation
      updateSelect(domElement, value, defaultValue, multiple, wasMultiple);
      return;
    }
    case 'textarea': {
      let value = null;
      let defaultValue = null;
      for (const propKey in lastProps) {
        const lastProp = lastProps[propKey];
        if (
          lastProps.hasOwnProperty(propKey) &&
          lastProp != null &&
          !nextProps.hasOwnProperty(propKey)
        ) {
          switch (propKey) {
            case 'value': {
              // This is handled by updateTextarea below.
              break;
            }
            case 'children': {
              // TODO: This doesn't actually do anything if it updates.
              break;
            }
            // defaultValue is ignored by setProp
            default: {
              setProp(domElement, tag, propKey, null, nextProps, lastProp);
            }
          }
        }
      }
      for (const propKey in nextProps) {
        const nextProp = nextProps[propKey];
        const lastProp = lastProps[propKey];
        if (
          nextProps.hasOwnProperty(propKey) &&
          (nextProp != null || lastProp != null)
        ) {
          switch (propKey) {
            case 'value': {
              if (nextProp !== lastProp) {
                trackHostMutation();
              }
              value = nextProp;
              // This is handled by updateTextarea below.
              break;
            }
            case 'defaultValue': {
              if (nextProp !== lastProp) {
                trackHostMutation();
              }
              defaultValue = nextProp;
              break;
            }
            case 'children': {
              // TODO: This doesn't actually do anything if it updates.
              break;
            }
            case 'dangerouslySetInnerHTML': {
              if (nextProp != null) {
                // TODO: Do we really need a special error message for this. It's also pretty blunt.
                throw new Error(
                  '`dangerouslySetInnerHTML` does not make sense on <textarea>.',
                );
              }
              break;
            }
            default: {
              if (nextProp !== lastProp)
                setProp(
                  domElement,
                  tag,
                  propKey,
                  nextProp,
                  nextProps,
                  lastProp,
                );
            }
          }
        }
      }
      updateTextarea(domElement, value, defaultValue);
      return;
    }
    case 'option': {
      for (const propKey in lastProps) {
        const lastProp = lastProps[propKey];
        if (
          lastProps.hasOwnProperty(propKey) &&
          lastProp != null &&
          !nextProps.hasOwnProperty(propKey)
        ) {
          switch (propKey) {
            case 'selected': {
              // TODO: Remove support for selected on option.
              (domElement: any).selected = false;
              break;
            }
            default: {
              setProp(domElement, tag, propKey, null, nextProps, lastProp);
            }
          }
        }
      }
      for (const propKey in nextProps) {
        const nextProp = nextProps[propKey];
        const lastProp = lastProps[propKey];
        if (
          nextProps.hasOwnProperty(propKey) &&
          nextProp !== lastProp &&
          (nextProp != null || lastProp != null)
        ) {
          switch (propKey) {
            case 'selected': {
              if (nextProp !== lastProp) {
                trackHostMutation();
              }
              // TODO: Remove support for selected on option.
              (domElement: any).selected =
                nextProp &&
                typeof nextProp !== 'function' &&
                typeof nextProp !== 'symbol';
              break;
            }
            default: {
              setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
            }
          }
        }
      }
      return;
    }
    case 'img':
    case 'link':
    case 'area':
    case 'base':
    case 'br':
    case 'col':
    case 'embed':
    case 'hr':
    case 'keygen':
    case 'meta':
    case 'param':
    case 'source':
    case 'track':
    case 'wbr':
    case 'menuitem': {
      // Void elements
      for (const propKey in lastProps) {
        const lastProp = lastProps[propKey];
        if (
          lastProps.hasOwnProperty(propKey) &&
          lastProp != null &&
          !nextProps.hasOwnProperty(propKey)
        ) {
          setProp(domElement, tag, propKey, null, nextProps, lastProp);
        }
      }
      for (const propKey in nextProps) {
        const nextProp = nextProps[propKey];
        const lastProp = lastProps[propKey];
        if (
          nextProps.hasOwnProperty(propKey) &&
          nextProp !== lastProp &&
          (nextProp != null || lastProp != null)
        ) {
          switch (propKey) {
            case 'children':
            case 'dangerouslySetInnerHTML': {
              if (nextProp != null) {
                // TODO: Can we make this a DEV warning to avoid this deny list?
                throw new Error(
                  `${tag} is a void element tag and must neither have \`children\` nor ` +
                    'use `dangerouslySetInnerHTML`.',
                );
              }
              break;
            }
            // defaultChecked and defaultValue are ignored by setProp
            default: {
              setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
            }
          }
        }
      }
      return;
    }
    default: {
      if (isCustomElement(tag, nextProps)) {
        for (const propKey in lastProps) {
          const lastProp = lastProps[propKey];
          if (
            lastProps.hasOwnProperty(propKey) &&
            lastProp !== undefined &&
            !nextProps.hasOwnProperty(propKey)
          ) {
            setPropOnCustomElement(
              domElement,
              tag,
              propKey,
              undefined,
              nextProps,
              lastProp,
            );
          }
        }
        for (const propKey in nextProps) {
          const nextProp = nextProps[propKey];
          const lastProp = lastProps[propKey];
          if (
            nextProps.hasOwnProperty(propKey) &&
            nextProp !== lastProp &&
            (nextProp !== undefined || lastProp !== undefined)
          ) {
            setPropOnCustomElement(
              domElement,
              tag,
              propKey,
              nextProp,
              nextProps,
              lastProp,
            );
          }
        }
        return;
      }
    }
  }

  for (const propKey in lastProps) {
    const lastProp = lastProps[propKey];
    if (
      lastProps.hasOwnProperty(propKey) &&
      lastProp != null &&
      !nextProps.hasOwnProperty(propKey)
    ) {
      setProp(domElement, tag, propKey, null, nextProps, lastProp);
    }
  }
  for (const propKey in nextProps) {
    const nextProp = nextProps[propKey];
    const lastProp = lastProps[propKey];
    if (
      nextProps.hasOwnProperty(propKey) &&
      nextProp !== lastProp &&
      (nextProp != null || lastProp != null)
    ) {
      setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
    }
  }
}

function getPossibleStandardName(propName: string): string | null {
  if (__DEV__) {
    const lowerCasedName = propName.toLowerCase();
    if (!possibleStandardNames.hasOwnProperty(lowerCasedName)) {
      return null;
    }
    return possibleStandardNames[lowerCasedName] || null;
  }
  return null;
}

function getPropNameFromAttributeName(attrName: string): string {
  switch (attrName) {
    case 'class':
      return 'className';
    case 'for':
      return 'htmlFor';
    // TODO: The rest of the aliases.
    default:
      return attrName;
  }
}

export function getPropsFromElement(domElement: Element): Object {
  const serverDifferences: {[propName: string]: mixed} = {};
  const attributes = domElement.attributes;
  for (let i = 0; i < attributes.length; i++) {
    const attr = attributes[i];
    serverDifferences[getPropNameFromAttributeName(attr.name)] =
      attr.name.toLowerCase() === 'style'
        ? getStylesObjectFromElement(domElement)
        : attr.value;
  }
  return serverDifferences;
}

function getStylesObjectFromElement(domElement: Element): {
  [styleName: string]: string,
} {
  const serverValueInObjectForm: {[prop: string]: string} = {};
  const style = ((domElement: any): HTMLElement).style;
  for (let i = 0; i < style.length; i++) {
    const styleName: string = style[i];
    // TODO: We should use the original prop value here if it is equivalent.
    // TODO: We could use the original client capitalization if the equivalent
    // other capitalization exists in the DOM.
    serverValueInObjectForm[styleName] = style.getPropertyValue(styleName);
  }
  return serverValueInObjectForm;
}

function diffHydratedStyles(
  domElement: Element,
  value: mixed,
  serverDifferences: {[propName: string]: mixed},
): void {
  if (value != null && typeof value !== 'object') {
    if (__DEV__) {
      console.error(
        'The `style` prop expects a mapping from style properties to values, ' +
          "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
          'using JSX.',
      );
    }
    return;
  }
  // First we compare the string form and see if it's equivalent.
  // This lets us bail out on anything that used to pass in this form.
  // It also lets us compare anything that's not parsed by this browser.
  const clientValue = createDangerousStringForStyles(value);
  const serverValue = domElement.getAttribute('style');

  if (serverValue === clientValue) {
    return;
  }
  const normalizedClientValue = normalizeMarkupForTextOrAttribute(clientValue);
  const normalizedServerValue = normalizeMarkupForTextOrAttribute(serverValue);
  if (normalizedServerValue === normalizedClientValue) {
    return;
  }

  // Otherwise, we create the object from the DOM for the diff view.
  serverDifferences.style = getStylesObjectFromElement(domElement);
}

function hydrateAttribute(
  domElement: Element,
  propKey: string,
  attributeName: string,
  value: any,
  extraAttributes: Set<string>,
  serverDifferences: {[propName: string]: mixed},
): void {
  extraAttributes.delete(attributeName);
  const serverValue = domElement.getAttribute(attributeName);
  if (serverValue === null) {
    switch (typeof value) {
      case 'undefined':
      case 'function':
      case 'symbol':
      case 'boolean':
        return;
    }
  } else {
    if (value == null) {
      // We had an attribute but shouldn't have had one, so read it
      // for the error message.
    } else {
      switch (typeof value) {
        case 'function':
        case 'symbol':
        case 'boolean':
          break;
        default: {
          if (__DEV__) {
            checkAttributeStringCoercion(value, propKey);
          }
          if (serverValue === '' + value) {
            return;
          }
        }
      }
    }
  }
  warnForPropDifference(propKey, serverValue, value, serverDifferences);
}

function hydrateBooleanAttribute(
  domElement: Element,
  propKey: string,
  attributeName: string,
  value: any,
  extraAttributes: Set<string>,
  serverDifferences: {[propName: string]: mixed},
): void {
  extraAttributes.delete(attributeName);
  const serverValue = domElement.getAttribute(attributeName);
  if (serverValue === null) {
    switch (typeof value) {
      case 'function':
      case 'symbol':
        return;
    }
    if (!value) {
      return;
    }
  } else {
    switch (typeof value) {
      case 'function':
      case 'symbol':
        break;
      default: {
        if (value) {
          // If this was a boolean, it doesn't matter what the value is
          // the fact that we have it is the same as the expected.
          // As long as it's positive.
          return;
        }
      }
    }
  }
  warnForPropDifference(propKey, serverValue, value, serverDifferences);
}

function hydrateOverloadedBooleanAttribute(
  domElement: Element,
  propKey: string,
  attributeName: string,
  value: any,
  extraAttributes: Set<string>,
  serverDifferences: {[propName: string]: mixed},
): void {
  extraAttributes.delete(attributeName);
  const serverValue = domElement.getAttribute(attributeName);
  if (serverValue === null) {
    switch (typeof value) {
      case 'undefined':
      case 'function':
      case 'symbol':
        return;
      default:
        if (value === false) {
          return;
        }
    }
  } else {
    if (value == null) {
      // We had an attribute but shouldn't have had one, so read it
      // for the error message.
    } else {
      switch (typeof value) {
        case 'function':
        case 'symbol':
          break;
        case 'boolean':
          if (value === true && serverValue === '') {
            return;
          }
          break;
        default: {
          if (__DEV__) {
            checkAttributeStringCoercion(value, propKey);
          }
          if (serverValue === '' + value) {
            return;
          }
        }
      }
    }
  }
  warnForPropDifference(propKey, serverValue, value, serverDifferences);
}

function hydrateBooleanishAttribute(
  domElement: Element,
  propKey: string,
  attributeName: string,
  value: any,
  extraAttributes: Set<string>,
  serverDifferences: {[propName: string]: mixed},
): void {
  extraAttributes.delete(attributeName);
  const serverValue = domElement.getAttribute(attributeName);
  if (serverValue === null) {
    switch (typeof value) {
      case 'undefined':
      case 'function':
      case 'symbol':
        return;
    }
  } else {
    if (value == null) {
      // We had an attribute but shouldn't have had one, so read it
      // for the error message.
    } else {
      switch (typeof value) {
        case 'function':
        case 'symbol':
          break;
        default: {
          if (__DEV__) {
            checkAttributeStringCoercion(value, attributeName);
          }
          if (serverValue === '' + (value: any)) {
            return;
          }
        }
      }
    }
  }
  warnForPropDifference(propKey, serverValue, value, serverDifferences);
}

function hydrateNumericAttribute(
  domElement: Element,
  propKey: string,
  attributeName: string,
  value: any,
  extraAttributes: Set<string>,
  serverDifferences: {[propName: string]: mixed},
): void {
  extraAttributes.delete(attributeName);
  const serverValue = domElement.getAttribute(attributeName);
  if (serverValue === null) {
    switch (typeof value) {
      case 'undefined':
      case 'function':
      case 'symbol':
      case 'boolean':
        return;
      default:
        if (isNaN(value)) {
          return;
        }
    }
  } else {
    if (value == null) {
      // We had an attribute but shouldn't have had one, so read it
      // for the error message.
    } else {
      switch (typeof value) {
        case 'function':
        case 'symbol':
        case 'boolean':
          break;
        default: {
          if (isNaN(value)) {
            // We had an attribute but shouldn't have had one, so read it
            // for the error message.
            break;
          }
          if (__DEV__) {
            checkAttributeStringCoercion(value, propKey);
          }
          if (serverValue === '' + value) {
            return;
          }
        }
      }
    }
  }
  warnForPropDifference(propKey, serverValue, value, serverDifferences);
}

function hydratePositiveNumericAttribute(
  domElement: Element,
  propKey: string,
  attributeName: string,
  value: any,
  extraAttributes: Set<string>,
  serverDifferences: {[propName: string]: mixed},
): void {
  extraAttributes.delete(attributeName);
  const serverValue = domElement.getAttribute(attributeName);
  if (serverValue === null) {
    switch (typeof value) {
      case 'undefined':
      case 'function':
      case 'symbol':
      case 'boolean':
        return;
      default:
        if (isNaN(value) || value < 1) {
          return;
        }
    }
  } else {
    if (value == null) {
      // We had an attribute but shouldn't have had one, so read it
      // for the error message.
    } else {
      switch (typeof value) {
        case 'function':
        case 'symbol':
        case 'boolean':
          break;
        default: {
          if (isNaN(value) || value < 1) {
            // We had an attribute but shouldn't have had one, so read it
            // for the error message.
            break;
          }
          if (__DEV__) {
            checkAttributeStringCoercion(value, propKey);
          }
          if (serverValue === '' + value) {
            return;
          }
        }
      }
    }
  }
  warnForPropDifference(propKey, serverValue, value, serverDifferences);
}

function hydrateSanitizedAttribute(
  domElement: Element,
  propKey: string,
  attributeName: string,
  value: any,
  extraAttributes: Set<string>,
  serverDifferences: {[propName: string]: mixed},
): void {
  extraAttributes.delete(attributeName);
  const serverValue = domElement.getAttribute(attributeName);
  if (serverValue === null) {
    switch (typeof value) {
      case 'undefined':
      case 'function':
      case 'symbol':
      case 'boolean':
        return;
    }
  } else {
    if (value == null) {
      // We had an attribute but shouldn't have had one, so read it
      // for the error message.
    } else {
      switch (typeof value) {
        case 'function':
        case 'symbol':
        case 'boolean':
          break;
        default: {
          if (__DEV__) {
            checkAttributeStringCoercion(value, propKey);
          }
          const sanitizedValue = sanitizeURL('' + value);
          if (serverValue === sanitizedValue) {
            return;
          }
        }
      }
    }
  }
  warnForPropDifference(propKey, serverValue, value, serverDifferences);
}

function hydrateSrcObjectAttribute(
  domElement: Element,
  value: Blob,
  extraAttributes: Set<string>,
  serverDifferences: {[propName: string]: mixed},
): void {
  const attributeName = 'src';
  extraAttributes.delete(attributeName);
  const serverValue = domElement.getAttribute(attributeName);
  if (serverValue != null && value != null) {
    const size = value.size;
    const type = value.type;
    if (typeof size === 'number' && typeof type === 'string') {
      if (serverValue.indexOf('data:' + type + ';base64,') === 0) {
        // For Blobs we don't bother reading the actual data but just diff by checking if
        // the byte length size of the Blob maches the length of the data url.
        const prefixLength = 5 + type.length + 8;
        let byteLength = ((serverValue.length - prefixLength) / 4) * 3;
        if (serverValue[serverValue.length - 1] === '=') {
          byteLength--;
        }
        if (serverValue[serverValue.length - 2] === '=') {
          byteLength--;
        }
        if (byteLength === size) {
          return;
        }
      }
    }
  }
  warnForPropDifference('src', serverValue, value, serverDifferences);
}

function diffHydratedCustomComponent(
  domElement: Element,
  tag: string,
  props: Object,
  hostContext: HostContext,
  extraAttributes: Set<string>,
  serverDifferences: {[propName: string]: mixed},
) {
  for (const propKey in props) {
    if (!props.hasOwnProperty(propKey)) {
      continue;
    }
    const value = props[propKey];
    if (value == null) {
      continue;
    }
    if (registrationNameDependencies.hasOwnProperty(propKey)) {
      if (typeof value !== 'function') {
        warnForInvalidEventListener(propKey, value);
      }
      continue;
    }
    if (props.suppressHydrationWarning === true) {
      // Don't bother comparing. We're ignoring all these warnings.
      continue;
    }
    // Validate that the properties correspond to their expected values.
    switch (propKey) {
      case 'children': {
        if (typeof value === 'string' || typeof value === 'number') {
          warnForPropDifference(
            'children',
            domElement.textContent,
            value,
            serverDifferences,
          );
        }
        continue;
      }
      // Checked above already
      case 'suppressContentEditableWarning':
      case 'suppressHydrationWarning':
      case 'defaultValue':
      case 'defaultChecked':
      case 'innerHTML':
      case 'ref':
        // Noop
        continue;
      case 'dangerouslySetInnerHTML':
        const serverHTML = domElement.innerHTML;
        const nextHtml = value ? value.__html : undefined;
        if (nextHtml != null) {
          const expectedHTML = normalizeHTML(domElement, nextHtml);
          warnForPropDifference(
            propKey,
            serverHTML,
            expectedHTML,
            serverDifferences,
          );
        }
        continue;
      case 'style':
        extraAttributes.delete(propKey);
        diffHydratedStyles(domElement, value, serverDifferences);
        continue;
      case 'offsetParent':
      case 'offsetTop':
      case 'offsetLeft':
      case 'offsetWidth':
      case 'offsetHeight':
      case 'isContentEditable':
      case 'outerText':
      case 'outerHTML':
        extraAttributes.delete(propKey.toLowerCase());
        if (__DEV__) {
          console.error(
            'Assignment to read-only property will result in a no-op: `%s`',
            propKey,
          );
        }
        continue;
      // Fall through
      case 'className':
        // className is a special cased property on the server to render as an attribute.
        extraAttributes.delete('class');
        const serverValue = getValueForAttributeOnCustomComponent(
          domElement,
          'class',
          value,
        );
        warnForPropDifference(
          'className',
          serverValue,
          value,
          serverDifferences,
        );
        continue;
      default: {
        // This is a DEV-only path
        const hostContextDev: HostContextDev = (hostContext: any);
        const hostContextProd = hostContextDev.context;
        if (
          hostContextProd === HostContextNamespaceNone &&
          tag !== 'svg' &&
          tag !== 'math'
        ) {
          extraAttributes.delete(propKey.toLowerCase());
        } else {
          extraAttributes.delete(propKey);
        }
        const valueOnCustomComponent = getValueForAttributeOnCustomComponent(
          domElement,
          propKey,
          value,
        );
        warnForPropDifference(
          propKey,
          valueOnCustomComponent,
          value,
          serverDifferences,
        );
      }
    }
  }
}

// This is the exact URL string we expect that Fizz renders if we provide a function action.
// We use this for hydration warnings. It needs to be in sync with Fizz. Maybe makes sense
// as a shared module for that reason.
const EXPECTED_FORM_ACTION_URL =
  // eslint-disable-next-line no-script-url
  "javascript:throw new Error('React form unexpectedly submitted.')";

function diffHydratedGenericElement(
  domElement: Element,
  tag: string,
  props: Object,
  hostContext: HostContext,
  extraAttributes: Set<string>,
  serverDifferences: {[propName: string]: mixed},
) {
  for (const propKey in props) {
    if (!props.hasOwnProperty(propKey)) {
      continue;
    }
    const value = props[propKey];
    if (value == null) {
      continue;
    }
    if (registrationNameDependencies.hasOwnProperty(propKey)) {
      if (typeof value !== 'function') {
        warnForInvalidEventListener(propKey, value);
      }
      continue;
    }
    if (props.suppressHydrationWarning === true) {
      // Don't bother comparing. We're ignoring all these warnings.
      continue;
    }
    // Validate that the properties correspond to their expected values.
    switch (propKey) {
      case 'children': {
        if (typeof value === 'string' || typeof value === 'number') {
          warnForPropDifference(
            'children',
            domElement.textContent,
            value,
            serverDifferences,
          );
        }
        continue;
      }
      // Checked above already
      case 'suppressContentEditableWarning':
      case 'suppressHydrationWarning':
      case 'value': // Controlled attributes are not validated
      case 'checked': // TODO: Only ignore them on controlled tags.
      case 'selected':
      case 'defaultValue':
      case 'defaultChecked':
      case 'innerHTML':
      case 'ref':
        // Noop
        continue;
      case 'dangerouslySetInnerHTML':
        const serverHTML = domElement.innerHTML;
        const nextHtml = value ? value.__html : undefined;
        if (nextHtml != null) {
          const expectedHTML = normalizeHTML(domElement, nextHtml);
          if (serverHTML !== expectedHTML) {
            serverDifferences[propKey] = {
              __html: serverHTML,
            };
          }
        }
        continue;
      case 'className':
        hydrateAttribute(
          domElement,
          propKey,
          'class',
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      case 'tabIndex':
        hydrateAttribute(
          domElement,
          propKey,
          'tabindex',
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      case 'style':
        extraAttributes.delete(propKey);
        diffHydratedStyles(domElement, value, serverDifferences);
        continue;
      case 'multiple': {
        extraAttributes.delete(propKey);
        const serverValue = (domElement: any).multiple;
        warnForPropDifference(propKey, serverValue, value, serverDifferences);
        continue;
      }
      case 'muted': {
        extraAttributes.delete(propKey);
        const serverValue = (domElement: any).muted;
        warnForPropDifference(propKey, serverValue, value, serverDifferences);
        continue;
      }
      case 'autoFocus': {
        extraAttributes.delete('autofocus');
        const serverValue = (domElement: any).autofocus;
        warnForPropDifference(propKey, serverValue, value, serverDifferences);
        continue;
      }
      case 'data':
        if (tag !== 'object') {
          extraAttributes.delete(propKey);
          const serverValue = (domElement: any).getAttribute('data');
          warnForPropDifference(propKey, serverValue, value, serverDifferences);
          continue;
        }
      // fallthrough
      case 'src': {
        if (enableSrcObject && typeof value === 'object' && value !== null) {
          // Some tags support object sources like Blob, File, MediaSource and MediaStream.
          if (tag === 'img' || tag === 'video' || tag === 'audio') {
            try {
              // Test if this is a compatible object
              URL.revokeObjectURL(URL.createObjectURL((value: any)));
              hydrateSrcObjectAttribute(
                domElement,
                value,
                extraAttributes,
                serverDifferences,
              );
              continue;
            } catch (x) {
              // If not, just fall through to the normal toString flow.
            }
          } else {
            if (__DEV__) {
              try {
                // This should always error.
                URL.revokeObjectURL(URL.createObjectURL((value: any)));
                if (tag === 'source') {
                  console.error(
                    'Passing Blob, MediaSource or MediaStream to <source src> is not supported. ' +
                      'Pass it directly to <img src>, <video src> or <audio src> instead.',
                  );
                } else {
                  console.error(
                    'Passing Blob, MediaSource or MediaStream to <%s src> is not supported.',
                    tag,
                  );
                }
              } catch (x) {}
            }
          }
        }
        // Fallthrough
      }
      case 'href':
        if (
          value === '' &&
          // <a href=""> is fine for "reload" links.
          !(tag === 'a' && propKey === 'href') &&
          !(tag === 'object' && propKey === 'data')
        ) {
          if (__DEV__) {
            if (propKey === 'src') {
              console.error(
                'An empty string ("") was passed to the %s attribute. ' +
                  'This may cause the browser to download the whole page again over the network. ' +
                  'To fix this, either do not render the element at all ' +
                  'or pass null to %s instead of an empty string.',
                propKey,
                propKey,
              );
            } else {
              console.error(
                'An empty string ("") was passed to the %s attribute. ' +
                  'To fix this, either do not render the element at all ' +
                  'or pass null to %s instead of an empty string.',
                propKey,
                propKey,
              );
            }
          }
          continue;
        }
        hydrateSanitizedAttribute(
          domElement,
          propKey,
          propKey,
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      case 'action':
      case 'formAction': {
        const serverValue = domElement.getAttribute(propKey);
        if (typeof value === 'function') {
          extraAttributes.delete(propKey.toLowerCase());
          // The server can set these extra properties to implement actions.
          // So we remove them from the extra attributes warnings.
          if (propKey === 'formAction') {
            extraAttributes.delete('name');
            extraAttributes.delete('formenctype');
            extraAttributes.delete('formmethod');
            extraAttributes.delete('formtarget');
          } else {
            extraAttributes.delete('enctype');
            extraAttributes.delete('method');
            extraAttributes.delete('target');
          }
          // Ideally we should be able to warn if the server value was not a function
          // however since the function can return any of these attributes any way it
          // wants as a custom progressive enhancement, there's nothing to compare to.
          // We can check if the function has the $FORM_ACTION property on the client
          // and if it's not, warn, but that's an unnecessary constraint that they
          // have to have the extra extension that doesn't do anything on the client.
          continue;
        } else if (serverValue === EXPECTED_FORM_ACTION_URL) {
          extraAttributes.delete(propKey.toLowerCase());
          warnForPropDifference(propKey, 'function', value, serverDifferences);
          continue;
        }
        hydrateSanitizedAttribute(
          domElement,
          propKey,
          propKey.toLowerCase(),
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      }
      case 'xlinkHref':
        hydrateSanitizedAttribute(
          domElement,
          propKey,
          'xlink:href',
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      case 'contentEditable': {
        // Lower-case Booleanish String
        hydrateBooleanishAttribute(
          domElement,
          propKey,
          'contenteditable',
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      }
      case 'spellCheck': {
        // Lower-case Booleanish String
        hydrateBooleanishAttribute(
          domElement,
          propKey,
          'spellcheck',
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      }
      case 'draggable':
      case 'autoReverse':
      case 'externalResourcesRequired':
      case 'focusable':
      case 'preserveAlpha': {
        // Case-sensitive Booleanish String
        hydrateBooleanishAttribute(
          domElement,
          propKey,
          propKey,
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      }
      case 'allowFullScreen':
      case 'async':
      case 'autoPlay':
      case 'controls':
      case 'default':
      case 'defer':
      case 'disabled':
      case 'disablePictureInPicture':
      case 'disableRemotePlayback':
      case 'formNoValidate':
      case 'hidden':
      case 'loop':
      case 'noModule':
      case 'noValidate':
      case 'open':
      case 'playsInline':
      case 'readOnly':
      case 'required':
      case 'reversed':
      case 'scoped':
      case 'seamless':
      case 'itemScope': {
        // Some of these need to be lower case to remove them from the extraAttributes list.
        hydrateBooleanAttribute(
          domElement,
          propKey,
          propKey.toLowerCase(),
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      }
      case 'capture':
      case 'download': {
        hydrateOverloadedBooleanAttribute(
          domElement,
          propKey,
          propKey,
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      }
      case 'cols':
      case 'rows':
      case 'size':
      case 'span': {
        hydratePositiveNumericAttribute(
          domElement,
          propKey,
          propKey,
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      }
      case 'rowSpan': {
        hydrateNumericAttribute(
          domElement,
          propKey,
          'rowspan',
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      }
      case 'start': {
        hydrateNumericAttribute(
          domElement,
          propKey,
          propKey,
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      }
      case 'xHeight':
        hydrateAttribute(
          domElement,
          propKey,
          'x-height',
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      case 'xlinkActuate':
        hydrateAttribute(
          domElement,
          propKey,
          'xlink:actuate',
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      case 'xlinkArcrole':
        hydrateAttribute(
          domElement,
          propKey,
          'xlink:arcrole',
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      case 'xlinkRole':
        hydrateAttribute(
          domElement,
          propKey,
          'xlink:role',
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      case 'xlinkShow':
        hydrateAttribute(
          domElement,
          propKey,
          'xlink:show',
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      case 'xlinkTitle':
        hydrateAttribute(
          domElement,
          propKey,
          'xlink:title',
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      case 'xlinkType':
        hydrateAttribute(
          domElement,
          propKey,
          'xlink:type',
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      case 'xmlBase':
        hydrateAttribute(
          domElement,
          propKey,
          'xml:base',
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      case 'xmlLang':
        hydrateAttribute(
          domElement,
          propKey,
          'xml:lang',
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      case 'xmlSpace':
        hydrateAttribute(
          domElement,
          propKey,
          'xml:space',
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      case 'inert':
        if (__DEV__) {
          if (
            value === '' &&
            !didWarnForNewBooleanPropsWithEmptyValue[propKey]
          ) {
            didWarnForNewBooleanPropsWithEmptyValue[propKey] = true;
            console.error(
              'Received an empty string for a boolean attribute `%s`. ' +
                'This will treat the attribute as if it were false. ' +
                'Either pass `false` to silence this warning, or ' +
                'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.',
              propKey,
            );
          }
        }
        hydrateBooleanAttribute(
          domElement,
          propKey,
          propKey,
          value,
          extraAttributes,
          serverDifferences,
        );
        continue;
      default: {
        if (
          // shouldIgnoreAttribute
          // We have already filtered out null/undefined and reserved words.
          propKey.length > 2 &&
          (propKey[0] === 'o' || propKey[0] === 'O') &&
          (propKey[1] === 'n' || propKey[1] === 'N')
        ) {
          continue;
        }
        const attributeName = getAttributeAlias(propKey);
        let isMismatchDueToBadCasing = false;

        // This is a DEV-only path
        const hostContextDev: HostContextDev = (hostContext: any);
        const hostContextProd = hostContextDev.context;

        if (
          hostContextProd === HostContextNamespaceNone &&
          tag !== 'svg' &&
          tag !== 'math'
        ) {
          extraAttributes.delete(attributeName.toLowerCase());
        } else {
          const standardName = getPossibleStandardName(propKey);
          if (standardName !== null && standardName !== propKey) {
            // If an SVG prop is supplied with bad casing, it will
            // be successfully parsed from HTML, but will produce a mismatch
            // (and would be incorrectly rendered on the client).
            // However, we already warn about bad casing elsewhere.
            // So we'll skip the misleading extra mismatch warning in this case.
            isMismatchDueToBadCasing = true;
            extraAttributes.delete(standardName);
          }
          extraAttributes.delete(attributeName);
        }
        const serverValue = getValueForAttribute(
          domElement,
          attributeName,
          value,
        );
        if (!isMismatchDueToBadCasing) {
          warnForPropDifference(propKey, serverValue, value, serverDifferences);
        }
      }
    }
  }
}

export function hydrateProperties(
  domElement: Element,
  tag: string,
  props: Object,
  hostContext: HostContext,
): boolean {
  if (__DEV__) {
    validatePropertiesInDevelopment(tag, props);
  }

  // TODO: Make sure that we check isMounted before firing any of these events.
  switch (tag) {
    case 'dialog':
      listenToNonDelegatedEvent('cancel', domElement);
      listenToNonDelegatedEvent('close', domElement);
      break;
    case 'iframe':
    case 'object':
    case 'embed':
      // We listen to this event in case to ensure emulated bubble
      // listeners still fire for the load event.
      listenToNonDelegatedEvent('load', domElement);
      break;
    case 'video':
    case 'audio':
      // We listen to these events in case to ensure emulated bubble
      // listeners still fire for all the media events.
      for (let i = 0; i < mediaEventTypes.length; i++) {
        listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
      }
      break;
    case 'source':
      // We listen to this event in case to ensure emulated bubble
      // listeners still fire for the error event.
      listenToNonDelegatedEvent('error', domElement);
      break;
    case 'img':
    case 'image':
    case 'link':
      // We listen to these events in case to ensure emulated bubble
      // listeners still fire for error and load events.
      listenToNonDelegatedEvent('error', domElement);
      listenToNonDelegatedEvent('load', domElement);
      break;
    case 'details':
      // We listen to this event in case to ensure emulated bubble
      // listeners still fire for the toggle event.
      listenToNonDelegatedEvent('toggle', domElement);
      break;
    case 'input':
      if (__DEV__) {
        checkControlledValueProps('input', props);
      }
      // We listen to this event in case to ensure emulated bubble
      // listeners still fire for the invalid event.
      listenToNonDelegatedEvent('invalid', domElement);
      // TODO: Make sure we check if this is still unmounted or do any clean
      // up necessary since we never stop tracking anymore.
      validateInputProps(domElement, props);
      // For input and textarea we current always set the value property at
      // post mount to force it to diverge from attributes. However, for
      // option and select we don't quite do the same thing and select
      // is not resilient to the DOM state changing so we don't do that here.
      // TODO: Consider not doing this for input and textarea.
      if (!enableHydrationChangeEvent) {
        initInput(
          domElement,
          props.value,
          props.defaultValue,
          props.checked,
          props.defaultChecked,
          props.type,
          props.name,
          true,
        );
      }
      break;
    case 'option':
      validateOptionProps(domElement, props);
      break;
    case 'select':
      if (__DEV__) {
        checkControlledValueProps('select', props);
      }
      // We listen to this event in case to ensure emulated bubble
      // listeners still fire for the invalid event.
      listenToNonDelegatedEvent('invalid', domElement);
      validateSelectProps(domElement, props);
      break;
    case 'textarea':
      if (__DEV__) {
        checkControlledValueProps('textarea', props);
      }
      // We listen to this event in case to ensure emulated bubble
      // listeners still fire for the invalid event.
      listenToNonDelegatedEvent('invalid', domElement);
      // TODO: Make sure we check if this is still unmounted or do any clean
      // up necessary since we never stop tracking anymore.
      validateTextareaProps(domElement, props);
      if (!enableHydrationChangeEvent) {
        initTextarea(
          domElement,
          props.value,
          props.defaultValue,
          props.children,
        );
      }
      break;
  }

  const children = props.children;
  // For text content children we compare against textContent. This
  // might match additional HTML that is hidden when we read it using
  // textContent. E.g. "foo" will match "f<span>oo</span>" but that still
  // satisfies our requirement. Our requirement is not to produce perfect
  // HTML and attributes. Ideally we should preserve structure but it's
  // ok not to if the visible content is still enough to indicate what
  // even listeners these nodes might be wired up to.
  // TODO: Warn if there is more than a single textNode as a child.
  // TODO: Should we use domElement.firstChild.nodeValue to compare?
  if (
    typeof children === 'string' ||
    typeof children === 'number' ||
    typeof children === 'bigint'
  ) {
    if (
      // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
      domElement.textContent !== '' + children &&
      props.suppressHydrationWarning !== true &&
      !checkForUnmatchedText(domElement.textContent, children)
    ) {
      return false;
    }
  }

  if (props.popover != null) {
    // We listen to this event in case to ensure emulated bubble
    // listeners still fire for the toggle event.
    listenToNonDelegatedEvent('beforetoggle', domElement);
    listenToNonDelegatedEvent('toggle', domElement);
  }

  if (props.onScroll != null) {
    listenToNonDelegatedEvent('scroll', domElement);
  }

  if (props.onScrollEnd != null) {
    listenToNonDelegatedEvent('scrollend', domElement);
    if (enableScrollEndPolyfill) {
      // For use by the polyfill.
      listenToNonDelegatedEvent('scroll', domElement);
    }
  }

  if (props.onClick != null) {
    // TODO: This cast may not be sound for SVG, MathML or custom elements.
    trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
  }

  return true;
}

export function diffHydratedProperties(
  domElement: Element,
  tag: string,
  props: Object,
  hostContext: HostContext,
): null | Object {
  const serverDifferences: {[propName: string]: mixed} = {};
  if (__DEV__) {
    const extraAttributes: Set<string> = new Set();
    const attributes = domElement.attributes;
    for (let i = 0; i < attributes.length; i++) {
      const name = attributes[i].name.toLowerCase();
      switch (name) {
        // Controlled attributes are not validated
        // TODO: Only ignore them on controlled tags.
        case 'value':
          break;
        case 'checked':
          break;
        case 'selected':
          break;
        case 'vt-name':
        case 'vt-update':
        case 'vt-enter':
        case 'vt-exit':
        case 'vt-share':
          if (enableViewTransition) {
            // View Transition annotations are expected from the Server Runtime.
            // However, if they're also specified on the client and don't match
            // that's an error.
            break;
          }
        // Fallthrough
        default:
          // Intentionally use the original name.
          // See discussion in https://github.com/facebook/react/pull/10676.
          extraAttributes.add(attributes[i].name);
      }
    }
    if (isCustomElement(tag, props)) {
      diffHydratedCustomComponent(
        domElement,
        tag,
        props,
        hostContext,
        extraAttributes,
        serverDifferences,
      );
    } else {
      diffHydratedGenericElement(
        domElement,
        tag,
        props,
        hostContext,
        extraAttributes,
        serverDifferences,
      );
    }
    if (extraAttributes.size > 0 && props.suppressHydrationWarning !== true) {
      warnForExtraAttributes(domElement, extraAttributes, serverDifferences);
    }
  }
  if (Object.keys(serverDifferences).length === 0) {
    return null;
  }
  return serverDifferences;
}

export function hydrateText(
  textNode: Text,
  text: string,
  parentProps: null | Object,
): boolean {
  const isDifferent = textNode.nodeValue !== text;
  if (
    isDifferent &&
    (parentProps === null || parentProps.suppressHydrationWarning !== true) &&
    !checkForUnmatchedText(textNode.nodeValue, text)
  ) {
    return false;
  }
  return true;
}

export function diffHydratedText(textNode: Text, text: string): null | string {
  if (textNode.nodeValue === text) {
    return null;
  }
  const normalizedClientText = normalizeMarkupForTextOrAttribute(text);
  const normalizedServerText = normalizeMarkupForTextOrAttribute(
    textNode.nodeValue,
  );
  if (normalizedServerText === normalizedClientText) {
    return null;
  }
  return textNode.nodeValue;
}

export function restoreControlledState(
  domElement: Element,
  tag: string,
  props: Object,
): void {
  switch (tag) {
    case 'input':
      restoreControlledInputState(domElement, props);
      return;
    case 'textarea':
      restoreControlledTextareaState(domElement, props);
      return;
    case 'select':
      restoreControlledSelectState(domElement, props);
      return;
  }
}