// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
 * @packageDocumentation
 * @module virtualdom
 */
import { ArrayExt } from '@lumino/algorithm';

/**
 * The names of the supported HTML5 DOM element attributes.
 *
 * This list is not all-encompassing, rather it attempts to define the
 * attribute names which are relevant for use in a virtual DOM context.
 * If a standardized or widely supported name is missing, please open
 * an issue to have it added.
 *
 * The attribute names were collected from the following sources:
 *   - https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
 *   - https://www.w3.org/TR/html5/index.html#attributes-1
 *   - https://html.spec.whatwg.org/multipage/indices.html#attributes-3
 */
export type ElementAttrNames =
  | 'abbr'
  | 'accept'
  | 'accept-charset'
  | 'accesskey'
  | 'action'
  | 'allowfullscreen'
  | 'alt'
  | 'autocomplete'
  | 'autofocus'
  | 'autoplay'
  | 'autosave'
  | 'checked'
  | 'cite'
  | 'cols'
  | 'colspan'
  | 'contenteditable'
  | 'controls'
  | 'coords'
  | 'crossorigin'
  | 'data'
  | 'datetime'
  | 'default'
  | 'dir'
  | 'dirname'
  | 'disabled'
  | 'download'
  | 'draggable'
  | 'dropzone'
  | 'enctype'
  | 'form'
  | 'formaction'
  | 'formenctype'
  | 'formmethod'
  | 'formnovalidate'
  | 'formtarget'
  | 'headers'
  | 'height'
  | 'hidden'
  | 'high'
  | 'href'
  | 'hreflang'
  | 'id'
  | 'inputmode'
  | 'integrity'
  | 'ismap'
  | 'kind'
  | 'label'
  | 'lang'
  | 'list'
  | 'loop'
  | 'low'
  | 'max'
  | 'maxlength'
  | 'media'
  | 'mediagroup'
  | 'method'
  | 'min'
  | 'minlength'
  | 'multiple'
  | 'muted'
  | 'name'
  | 'novalidate'
  | 'optimum'
  | 'pattern'
  | 'placeholder'
  | 'poster'
  | 'preload'
  | 'readonly'
  | 'rel'
  | 'required'
  | 'reversed'
  | 'rows'
  | 'rowspan'
  | 'sandbox'
  | 'scope'
  | 'selected'
  | 'shape'
  | 'size'
  | 'sizes'
  | 'span'
  | 'spellcheck'
  | 'src'
  | 'srcdoc'
  | 'srclang'
  | 'srcset'
  | 'start'
  | 'step'
  | 'tabindex'
  | 'target'
  | 'title'
  | 'type'
  | 'typemustmatch'
  | 'usemap'
  | 'value'
  | 'width'
  | 'wrap';

/**
 * The names of ARIA attributes for HTML elements.
 *
 * The attribute names are collected from
 * https://www.w3.org/TR/html5/infrastructure.html#element-attrdef-aria-role
 */
export type ARIAAttrNames =
  | 'aria-activedescendant'
  | 'aria-atomic'
  | 'aria-autocomplete'
  | 'aria-busy'
  | 'aria-checked'
  | 'aria-colcount'
  | 'aria-colindex'
  | 'aria-colspan'
  | 'aria-controls'
  | 'aria-current'
  | 'aria-describedby'
  | 'aria-details'
  | 'aria-dialog'
  | 'aria-disabled'
  | 'aria-dropeffect'
  | 'aria-errormessage'
  | 'aria-expanded'
  | 'aria-flowto'
  | 'aria-grabbed'
  | 'aria-haspopup'
  | 'aria-hidden'
  | 'aria-invalid'
  | 'aria-keyshortcuts'
  | 'aria-label'
  | 'aria-labelledby'
  | 'aria-level'
  | 'aria-live'
  | 'aria-multiline'
  | 'aria-multiselectable'
  | 'aria-orientation'
  | 'aria-owns'
  | 'aria-placeholder'
  | 'aria-posinset'
  | 'aria-pressed'
  | 'aria-readonly'
  | 'aria-relevant'
  | 'aria-required'
  | 'aria-roledescription'
  | 'aria-rowcount'
  | 'aria-rowindex'
  | 'aria-rowspan'
  | 'aria-selected'
  | 'aria-setsize'
  | 'aria-sort'
  | 'aria-valuemax'
  | 'aria-valuemin'
  | 'aria-valuenow'
  | 'aria-valuetext'
  | 'role';

/**
 * The names of the supported HTML5 CSS property names.
 *
 * If a standardized or widely supported name is missing, please open
 * an issue to have it added.
 *
 * The property names were collected from the following sources:
 *   - TypeScript's `lib.dom.d.ts` file
 */
export type CSSPropertyNames =
  | 'alignContent'
  | 'alignItems'
  | 'alignSelf'
  | 'alignmentBaseline'
  | 'animation'
  | 'animationDelay'
  | 'animationDirection'
  | 'animationDuration'
  | 'animationFillMode'
  | 'animationIterationCount'
  | 'animationName'
  | 'animationPlayState'
  | 'animationTimingFunction'
  | 'backfaceVisibility'
  | 'background'
  | 'backgroundAttachment'
  | 'backgroundClip'
  | 'backgroundColor'
  | 'backgroundImage'
  | 'backgroundOrigin'
  | 'backgroundPosition'
  | 'backgroundPositionX'
  | 'backgroundPositionY'
  | 'backgroundRepeat'
  | 'backgroundSize'
  | 'baselineShift'
  | 'border'
  | 'borderBottom'
  | 'borderBottomColor'
  | 'borderBottomLeftRadius'
  | 'borderBottomRightRadius'
  | 'borderBottomStyle'
  | 'borderBottomWidth'
  | 'borderCollapse'
  | 'borderColor'
  | 'borderImage'
  | 'borderImageOutset'
  | 'borderImageRepeat'
  | 'borderImageSlice'
  | 'borderImageSource'
  | 'borderImageWidth'
  | 'borderLeft'
  | 'borderLeftColor'
  | 'borderLeftStyle'
  | 'borderLeftWidth'
  | 'borderRadius'
  | 'borderRight'
  | 'borderRightColor'
  | 'borderRightStyle'
  | 'borderRightWidth'
  | 'borderSpacing'
  | 'borderStyle'
  | 'borderTop'
  | 'borderTopColor'
  | 'borderTopLeftRadius'
  | 'borderTopRightRadius'
  | 'borderTopStyle'
  | 'borderTopWidth'
  | 'borderWidth'
  | 'bottom'
  | 'boxShadow'
  | 'boxSizing'
  | 'breakAfter'
  | 'breakBefore'
  | 'breakInside'
  | 'captionSide'
  | 'clear'
  | 'clip'
  | 'clipPath'
  | 'clipRule'
  | 'color'
  | 'colorInterpolationFilters'
  | 'columnCount'
  | 'columnFill'
  | 'columnGap'
  | 'columnRule'
  | 'columnRuleColor'
  | 'columnRuleStyle'
  | 'columnRuleWidth'
  | 'columnSpan'
  | 'columnWidth'
  | 'columns'
  | 'content'
  | 'counterIncrement'
  | 'counterReset'
  | 'cssFloat'
  | 'cssText'
  | 'cursor'
  | 'direction'
  | 'display'
  | 'dominantBaseline'
  | 'emptyCells'
  | 'enableBackground'
  | 'fill'
  | 'fillOpacity'
  | 'fillRule'
  | 'filter'
  | 'flex'
  | 'flexBasis'
  | 'flexDirection'
  | 'flexFlow'
  | 'flexGrow'
  | 'flexShrink'
  | 'flexWrap'
  | 'floodColor'
  | 'floodOpacity'
  | 'font'
  | 'fontFamily'
  | 'fontFeatureSettings'
  | 'fontSize'
  | 'fontSizeAdjust'
  | 'fontStretch'
  | 'fontStyle'
  | 'fontVariant'
  | 'fontWeight'
  | 'glyphOrientationHorizontal'
  | 'glyphOrientationVertical'
  | 'height'
  | 'imeMode'
  | 'justifyContent'
  | 'kerning'
  | 'left'
  | 'letterSpacing'
  | 'lightingColor'
  | 'lineHeight'
  | 'listStyle'
  | 'listStyleImage'
  | 'listStylePosition'
  | 'listStyleType'
  | 'margin'
  | 'marginBottom'
  | 'marginLeft'
  | 'marginRight'
  | 'marginTop'
  | 'marker'
  | 'markerEnd'
  | 'markerMid'
  | 'markerStart'
  | 'mask'
  | 'maxHeight'
  | 'maxWidth'
  | 'minHeight'
  | 'minWidth'
  | 'msContentZoomChaining'
  | 'msContentZoomLimit'
  | 'msContentZoomLimitMax'
  | 'msContentZoomLimitMin'
  | 'msContentZoomSnap'
  | 'msContentZoomSnapPoints'
  | 'msContentZoomSnapType'
  | 'msContentZooming'
  | 'msFlowFrom'
  | 'msFlowInto'
  | 'msFontFeatureSettings'
  | 'msGridColumn'
  | 'msGridColumnAlign'
  | 'msGridColumnSpan'
  | 'msGridColumns'
  | 'msGridRow'
  | 'msGridRowAlign'
  | 'msGridRowSpan'
  | 'msGridRows'
  | 'msHighContrastAdjust'
  | 'msHyphenateLimitChars'
  | 'msHyphenateLimitLines'
  | 'msHyphenateLimitZone'
  | 'msHyphens'
  | 'msImeAlign'
  | 'msOverflowStyle'
  | 'msScrollChaining'
  | 'msScrollLimit'
  | 'msScrollLimitXMax'
  | 'msScrollLimitXMin'
  | 'msScrollLimitYMax'
  | 'msScrollLimitYMin'
  | 'msScrollRails'
  | 'msScrollSnapPointsX'
  | 'msScrollSnapPointsY'
  | 'msScrollSnapType'
  | 'msScrollSnapX'
  | 'msScrollSnapY'
  | 'msScrollTranslation'
  | 'msTextCombineHorizontal'
  | 'msTextSizeAdjust'
  | 'msTouchAction'
  | 'msTouchSelect'
  | 'msUserSelect'
  | 'msWrapFlow'
  | 'msWrapMargin'
  | 'msWrapThrough'
  | 'opacity'
  | 'order'
  | 'orphans'
  | 'outline'
  | 'outlineColor'
  | 'outlineStyle'
  | 'outlineWidth'
  | 'overflow'
  | 'overflowX'
  | 'overflowY'
  | 'padding'
  | 'paddingBottom'
  | 'paddingLeft'
  | 'paddingRight'
  | 'paddingTop'
  | 'pageBreakAfter'
  | 'pageBreakBefore'
  | 'pageBreakInside'
  | 'perspective'
  | 'perspectiveOrigin'
  | 'pointerEvents'
  | 'position'
  | 'quotes'
  | 'resize'
  | 'right'
  | 'rubyAlign'
  | 'rubyOverhang'
  | 'rubyPosition'
  | 'stopColor'
  | 'stopOpacity'
  | 'stroke'
  | 'strokeDasharray'
  | 'strokeDashoffset'
  | 'strokeLinecap'
  | 'strokeLinejoin'
  | 'strokeMiterlimit'
  | 'strokeOpacity'
  | 'strokeWidth'
  | 'tableLayout'
  | 'textAlign'
  | 'textAlignLast'
  | 'textAnchor'
  | 'textDecoration'
  | 'textIndent'
  | 'textJustify'
  | 'textKashida'
  | 'textKashidaSpace'
  | 'textOverflow'
  | 'textShadow'
  | 'textTransform'
  | 'textUnderlinePosition'
  | 'top'
  | 'touchAction'
  | 'transform'
  | 'transformOrigin'
  | 'transformStyle'
  | 'transition'
  | 'transitionDelay'
  | 'transitionDuration'
  | 'transitionProperty'
  | 'transitionTimingFunction'
  | 'unicodeBidi'
  | 'verticalAlign'
  | 'visibility'
  | 'webkitAlignContent'
  | 'webkitAlignItems'
  | 'webkitAlignSelf'
  | 'webkitAnimation'
  | 'webkitAnimationDelay'
  | 'webkitAnimationDirection'
  | 'webkitAnimationDuration'
  | 'webkitAnimationFillMode'
  | 'webkitAnimationIterationCount'
  | 'webkitAnimationName'
  | 'webkitAnimationPlayState'
  | 'webkitAnimationTimingFunction'
  | 'webkitAppearance'
  | 'webkitBackfaceVisibility'
  | 'webkitBackgroundClip'
  | 'webkitBackgroundOrigin'
  | 'webkitBackgroundSize'
  | 'webkitBorderBottomLeftRadius'
  | 'webkitBorderBottomRightRadius'
  | 'webkitBorderImage'
  | 'webkitBorderRadius'
  | 'webkitBorderTopLeftRadius'
  | 'webkitBorderTopRightRadius'
  | 'webkitBoxAlign'
  | 'webkitBoxDirection'
  | 'webkitBoxFlex'
  | 'webkitBoxOrdinalGroup'
  | 'webkitBoxOrient'
  | 'webkitBoxPack'
  | 'webkitBoxSizing'
  | 'webkitColumnBreakAfter'
  | 'webkitColumnBreakBefore'
  | 'webkitColumnBreakInside'
  | 'webkitColumnCount'
  | 'webkitColumnGap'
  | 'webkitColumnRule'
  | 'webkitColumnRuleColor'
  | 'webkitColumnRuleStyle'
  | 'webkitColumnRuleWidth'
  | 'webkitColumnSpan'
  | 'webkitColumnWidth'
  | 'webkitColumns'
  | 'webkitFilter'
  | 'webkitFlex'
  | 'webkitFlexBasis'
  | 'webkitFlexDirection'
  | 'webkitFlexFlow'
  | 'webkitFlexGrow'
  | 'webkitFlexShrink'
  | 'webkitFlexWrap'
  | 'webkitJustifyContent'
  | 'webkitOrder'
  | 'webkitPerspective'
  | 'webkitPerspectiveOrigin'
  | 'webkitTapHighlightColor'
  | 'webkitTextFillColor'
  | 'webkitTextSizeAdjust'
  | 'webkitTransform'
  | 'webkitTransformOrigin'
  | 'webkitTransformStyle'
  | 'webkitTransition'
  | 'webkitTransitionDelay'
  | 'webkitTransitionDuration'
  | 'webkitTransitionProperty'
  | 'webkitTransitionTimingFunction'
  | 'webkitUserModify'
  | 'webkitUserSelect'
  | 'webkitWritingMode'
  | 'whiteSpace'
  | 'widows'
  | 'width'
  | 'wordBreak'
  | 'wordSpacing'
  | 'wordWrap'
  | 'writingMode'
  | 'zIndex'
  | 'zoom';

/**
 * A mapping of inline event name to event object type.
 *
 * This mapping is used to create the event listener properties for
 * the virtual DOM element attributes object. If a standardized or
 * widely supported name is missing, please open an issue to have it
 * added.
 *
 * The event names were collected from the following sources:
 *   - TypeScript's `lib.dom.d.ts` file
 *   - https://www.w3.org/TR/html5/index.html#attributes-1
 *   - https://html.spec.whatwg.org/multipage/webappapis.html#idl-definitions
 */
export type ElementEventMap = {
  onabort: UIEvent;
  onauxclick: MouseEvent;
  onblur: FocusEvent;
  oncanplay: Event;
  oncanplaythrough: Event;
  onchange: Event;
  onclick: MouseEvent;
  oncontextmenu: PointerEvent;
  oncopy: ClipboardEvent;
  oncuechange: Event;
  oncut: ClipboardEvent;
  ondblclick: MouseEvent;
  ondrag: DragEvent;
  ondragend: DragEvent;
  ondragenter: DragEvent;
  ondragexit: DragEvent;
  ondragleave: DragEvent;
  ondragover: DragEvent;
  ondragstart: DragEvent;
  ondrop: DragEvent;
  ondurationchange: Event;
  onemptied: Event;
  onended: ErrorEvent;
  onerror: ErrorEvent;
  onfocus: FocusEvent;
  oninput: Event;
  oninvalid: Event;
  onkeydown: KeyboardEvent;
  onkeypress: KeyboardEvent;
  onkeyup: KeyboardEvent;
  onload: Event;
  onloadeddata: Event;
  onloadedmetadata: Event;
  onloadend: Event;
  onloadstart: Event;
  onmousedown: MouseEvent;
  onmouseenter: MouseEvent;
  onmouseleave: MouseEvent;
  onmousemove: MouseEvent;
  onmouseout: MouseEvent;
  onmouseover: MouseEvent;
  onmouseup: MouseEvent;
  onmousewheel: WheelEvent;
  onpaste: ClipboardEvent;
  onpause: Event;
  onplay: Event;
  onplaying: Event;
  onpointercancel: PointerEvent;
  onpointerdown: PointerEvent;
  onpointerenter: PointerEvent;
  onpointerleave: PointerEvent;
  onpointermove: PointerEvent;
  onpointerout: PointerEvent;
  onpointerover: PointerEvent;
  onpointerup: PointerEvent;
  onprogress: ProgressEvent;
  onratechange: Event;
  onreset: Event;
  onscroll: UIEvent;
  onseeked: Event;
  onseeking: Event;
  onselect: UIEvent;
  onselectstart: Event;
  onstalled: Event;
  onsubmit: Event;
  onsuspend: Event;
  ontimeupdate: Event;
  onvolumechange: Event;
  onwaiting: Event;
};

/**
 * An object which represents a dataset for a virtual DOM element.
 *
 * The names of the dataset properties will be automatically prefixed
 * with `data-` before being added to the node, e.g. `{ thing: '12' }`
 * will be rendered as `data-thing='12'` in the DOM element.
 *
 * Dataset property names should not contain spaces.
 */
export type ElementDataset = {
  readonly [name: string]: string;
};

/**
 * The inline style for for a virtual DOM element.
 *
 * Style attributes use the JS camel-cased property names instead of
 * the CSS hyphenated names for performance and security.
 */
export type ElementInlineStyle = {
  readonly [T in CSSPropertyNames]?: string;
};

/**
 * The ARIA attributes for a virtual element node.
 *
 * These are the attributes which are applied to a real DOM element via
 * `element.setAttribute()`. The supported attribute names are defined
 * by the `ARIAAttrNames` type.
 */
export type ElementARIAAttrs = {
  readonly [T in ARIAAttrNames]?: string;
};

/**
 * The base attributes for a virtual element node.
 *
 * These are the attributes which are applied to a real DOM element via
 * `element.setAttribute()`. The supported attribute names are defined
 * by the `ElementAttrNames` type.
 *
 * Node attributes are specified using the lower-case HTML name instead
 * of the camel-case JS name due to browser inconsistencies in handling
 * the JS versions.
 */
export type ElementBaseAttrs = {
  readonly [T in ElementAttrNames]?: string;
};

/**
 * The inline event listener attributes for a virtual element node.
 *
 * The supported listeners are defined by the `ElementEventMap` type.
 */
export type ElementEventAttrs = {
  readonly [T in keyof ElementEventMap]?: (
    this: HTMLElement,
    event: ElementEventMap[T]
  ) => any;
};

/**
 * The special-cased attributes for a virtual element node.
 */
export type ElementSpecialAttrs = {
  /**
   * The key id for the virtual element node.
   *
   * If a node is given a key id, the generated DOM node will not be
   * recreated during a rendering update if it only moves among its
   * siblings in the render tree.
   *
   * In general, reordering child nodes will cause the nodes to be
   * completely re-rendered. Keys allow this to be optimized away.
   *
   * If a key is provided, it must be unique among sibling nodes.
   */
  readonly key?: string;

  /**
   * The JS-safe name for the HTML `class` attribute.
   */
  readonly className?: string;

  /**
   * The JS-safe name for the HTML `for` attribute.
   */
  readonly htmlFor?: string;

  /**
   * The dataset for the rendered DOM element.
   */
  readonly dataset?: ElementDataset;

  /**
   * The inline style for the rendered DOM element.
   */
  readonly style?: ElementInlineStyle;
};

/**
 * The full set of attributes supported by a virtual element node.
 *
 * This is the combination of the base element attributes, the the ARIA attributes,
 * the inline element event listeners, and the special element attributes.
 */
export type ElementAttrs = ElementBaseAttrs &
  ElementARIAAttrs &
  ElementEventAttrs &
  ElementSpecialAttrs;

/**
 * A virtual node which represents plain text content.
 *
 * #### Notes
 * User code will not typically create a `VirtualText` node directly.
 * Instead, the `h()` function will be used to create an element tree.
 */
export class VirtualText {
  /**
   * The text content for the node.
   */
  readonly content: string;

  /**
   * The type of the node.
   *
   * This value can be used as a type guard for discriminating the
   * `VirtualNode` union type.
   */
  readonly type = 'text' as const;

  /**
   * Construct a new virtual text node.
   *
   * @param content - The text content for the node.
   */
  constructor(content: string) {
    this.content = content;
  }
}

/**
 * A virtual node which represents an HTML element.
 *
 * #### Notes
 * User code will not typically create a `VirtualElement` node directly.
 * Instead, the `h()` function will be used to create an element tree.
 */
export class VirtualElement {
  /**
   * The tag name for the element.
   */
  readonly tag: string;

  /**
   * The attributes for the element.
   */
  readonly attrs: ElementAttrs;

  /**
   * The children for the element.
   */
  readonly children: ReadonlyArray<VirtualNode>;

  /**
   * An optional custom renderer for the element's children. If set, on render
   * this element's DOM node and it's attrs will be created/updated as normal.
   * At that point the DOM node is handed off to the renderer.
   */
  readonly renderer: VirtualElement.IRenderer | undefined;

  /**
   * The type of the node.
   *
   * This value can be used as a type guard for discriminating the
   * `VirtualNode` union type.
   */
  readonly type = 'element' as const;

  /**
   * Construct a new virtual element node.
   *
   * @param tag - The element tag name.
   *
   * @param attrs - The element attributes.
   *
   * @param children - The element children.
   *
   * @param renderer - An optional custom renderer for the element.
   */
  constructor(
    tag: string,
    attrs: ElementAttrs,
    children: ReadonlyArray<VirtualNode>,
    renderer?: VirtualElement.IRenderer
  ) {
    this.tag = tag;
    this.attrs = attrs;
    this.children = children;

    this.renderer = renderer;
  }
}

export namespace VirtualElement {
  /**
   * A type describing a custom element renderer
   */
  export type IRenderer = {
    /**
     * Customize how a DOM node is rendered. If .renderer is set on a given
     * instance of VirtualElement, this function will be called every time
     * that VirtualElement is rendered.
     *
     * @param host - The actual DOM node created for a VirtualElement during
     * rendering.
     *
     * On render, host is created and its attrs are set/updated via
     * the standard routines in updateContent. host is then handed off to this
     * function.
     *
     * The render function is free to modify host. The only restriction is
     * is that render should not modify any attributes set by external
     * routines (ie updateContent), as this may cause thrashing when the
     * virtual element is next rendered.
     *
     * @param options - Will be populated with the .attrs and .children fields
     * set on the VirtualElement being rendered.
     */
    render: (
      host: HTMLElement,
      options?: { attrs?: ElementAttrs; children?: ReadonlyArray<VirtualNode> }
    ) => void;

    /**
     * Optional cleanup function for custom renderers. If the .renderer field
     * of a VirtualELement is set, and if .renderer.unrender is defined, when
     * the element is changed or removed its corresponding DOM element will be
     * passed to this function immediately before it is removed from the DOM.
     *
     * unrender is not required for for simple renderers, such as those
     * implemented using `document.createElement()`. However, for certain
     * rendering techniques explicit cleanup is required in order to avoid
     * resource leaks.
     *
     * For example, if render calls `ReactDOM.render(..., host)`, then
     * there has to also be a corresponding implementation of unrender that
     * calls `ReactDOM.unmountComponentAtNode(host)` in order to prevent
     * a memory leak.
     *
     * @param host - the DOM element to be removed.
     *
     * @param options - Will be populated with the .attrs and .children fields
     * set on the VirtualElement being unrendered.
     */
    unrender?: (
      host: HTMLElement,
      options?: { attrs?: ElementAttrs; children?: ReadonlyArray<VirtualNode> }
    ) => void;
  };
}

/**
 * DEPRECATED - use VirtualElement with a defined renderer param instead.
 * This class is provided as a backwards compatibility shim
 *
 * A "pass thru" virtual node whose children are managed by a render and an
 * unrender callback. The intent of this flavor of virtual node is to make
 * it easy to blend other kinds of virtualdom (eg React) into Phosphor's
 * virtualdom.
 *
 * #### Notes
 * User code will not typically create a `VirtualElementPass` node directly.
 * Instead, the `hpass()` function will be used to create an element tree.
 */
export class VirtualElementPass extends VirtualElement {
  /**
   * DEPRECATED - use VirtualElement with a defined renderer param instead
   *
   * Construct a new virtual element pass thru node.
   *
   * @param tag - the tag of the parent element of this node. Once the parent
   * element is rendered, it will be passed as an argument to
   * renderer.render
   *
   * @param attrs - attributes that will assigned to the
   * parent element
   *
   * @param renderer - an object with render and unrender
   * functions, each of which should take a single argument of type
   * HTMLElement and return nothing. If null, the parent element
   * will be rendered barren without any children.
   */
  constructor(
    tag: string,
    attrs: ElementAttrs,
    renderer: VirtualElementPass.IRenderer | null
  ) {
    super(tag, attrs, [], renderer || undefined);
  }
}

export namespace VirtualElementPass {
  /**
   * DEPRECATED - use VirtualElement.IRenderer instead
   *
   * A type describing a custom element renderer
   */
  export type IRenderer = VirtualElement.IRenderer;
}

/**
 * A type alias for a general virtual node.
 */
export type VirtualNode = VirtualElement | VirtualText;

/**
 * Create a new virtual element node.
 *
 * @param tag - The tag name for the element.
 *
 * @param attrs - The attributes for the element, if any.
 *
 * @param renderer - An optional custom renderer for the element.
 *
 * @param children - The children for the element, if any.
 *
 * @returns A new virtual element node for the given parameters.
 *
 * #### Notes
 * The children may be string literals, other virtual nodes, `null`, or
 * an array of those things. Strings are converted into text nodes, and
 * arrays are inlined as if the array contents were given as positional
 * arguments. This makes it simple to build up an array of children by
 * any desired means. `null` child values are simply ignored.
 *
 * A bound function for each HTML tag name is available as a static
 * function attached to the `h()` function. E.g. `h('div', ...)` is
 * equivalent to `h.div(...)`.
 */
export function h(tag: string, ...children: h.Child[]): VirtualElement;
export function h(
  tag: string,
  attrs: ElementAttrs,
  ...children: h.Child[]
): VirtualElement;
export function h(
  tag: string,
  renderer: VirtualElement.IRenderer,
  ...children: h.Child[]
): VirtualElement;
export function h(
  tag: string,
  attrs: ElementAttrs,
  renderer: VirtualElement.IRenderer,
  ...children: h.Child[]
): VirtualElement;
export function h(tag: string): VirtualElement {
  let attrs: ElementAttrs = {};
  let renderer: VirtualElement.IRenderer | undefined;
  let children: VirtualNode[] = [];
  for (let i = 1, n = arguments.length; i < n; ++i) {
    // eslint-disable-next-line prefer-rest-params
    let arg = arguments[i];
    if (typeof arg === 'string') {
      children.push(new VirtualText(arg));
    } else if (arg instanceof VirtualText) {
      children.push(arg);
    } else if (arg instanceof VirtualElement) {
      children.push(arg);
    } else if (arg instanceof Array) {
      extend(children, arg);
    } else if ((i === 1 || i === 2) && arg && typeof arg === 'object') {
      if ('render' in arg) {
        renderer = arg;
      } else {
        attrs = arg;
      }
    }
  }
  return new VirtualElement(tag, attrs, children, renderer);

  function extend(array: VirtualNode[], values: h.Child[]): void {
    for (let child of values) {
      if (typeof child === 'string') {
        array.push(new VirtualText(child));
      } else if (child instanceof VirtualText) {
        array.push(child);
      } else if (child instanceof VirtualElement) {
        array.push(child);
      }
    }
  }
}

/**
 * The namespace for the `h` function statics.
 */
export namespace h {
  /**
   * A type alias for the supported child argument types.
   */
  export type Child =
    | (string | VirtualNode | null)
    | Array<string | VirtualNode | null>;

  /**
   * A bound factory function for a specific `h()` tag.
   */
  export interface IFactory {
    (...children: Child[]): VirtualElement;
    (attrs: ElementAttrs, ...children: Child[]): VirtualElement;
    (
      renderer: VirtualElement.IRenderer,
      ...children: h.Child[]
    ): VirtualElement;
    (
      attrs: ElementAttrs,
      renderer: VirtualElement.IRenderer,
      ...children: h.Child[]
    ): VirtualElement;
  }

  export const a: IFactory = h.bind(undefined, 'a');
  export const abbr: IFactory = h.bind(undefined, 'abbr');
  export const address: IFactory = h.bind(undefined, 'address');
  export const area: IFactory = h.bind(undefined, 'area');
  export const article: IFactory = h.bind(undefined, 'article');
  export const aside: IFactory = h.bind(undefined, 'aside');
  export const audio: IFactory = h.bind(undefined, 'audio');
  export const b: IFactory = h.bind(undefined, 'b');
  export const bdi: IFactory = h.bind(undefined, 'bdi');
  export const bdo: IFactory = h.bind(undefined, 'bdo');
  export const blockquote: IFactory = h.bind(undefined, 'blockquote');
  export const br: IFactory = h.bind(undefined, 'br');
  export const button: IFactory = h.bind(undefined, 'button');
  export const canvas: IFactory = h.bind(undefined, 'canvas');
  export const caption: IFactory = h.bind(undefined, 'caption');
  export const cite: IFactory = h.bind(undefined, 'cite');
  export const code: IFactory = h.bind(undefined, 'code');
  export const col: IFactory = h.bind(undefined, 'col');
  export const colgroup: IFactory = h.bind(undefined, 'colgroup');
  export const data: IFactory = h.bind(undefined, 'data');
  export const datalist: IFactory = h.bind(undefined, 'datalist');
  export const dd: IFactory = h.bind(undefined, 'dd');
  export const del: IFactory = h.bind(undefined, 'del');
  export const dfn: IFactory = h.bind(undefined, 'dfn');
  export const div: IFactory = h.bind(undefined, 'div');
  export const dl: IFactory = h.bind(undefined, 'dl');
  export const dt: IFactory = h.bind(undefined, 'dt');
  export const em: IFactory = h.bind(undefined, 'em');
  export const embed: IFactory = h.bind(undefined, 'embed');
  export const fieldset: IFactory = h.bind(undefined, 'fieldset');
  export const figcaption: IFactory = h.bind(undefined, 'figcaption');
  export const figure: IFactory = h.bind(undefined, 'figure');
  export const footer: IFactory = h.bind(undefined, 'footer');
  export const form: IFactory = h.bind(undefined, 'form');
  export const h1: IFactory = h.bind(undefined, 'h1');
  export const h2: IFactory = h.bind(undefined, 'h2');
  export const h3: IFactory = h.bind(undefined, 'h3');
  export const h4: IFactory = h.bind(undefined, 'h4');
  export const h5: IFactory = h.bind(undefined, 'h5');
  export const h6: IFactory = h.bind(undefined, 'h6');
  export const header: IFactory = h.bind(undefined, 'header');
  export const hr: IFactory = h.bind(undefined, 'hr');
  export const i: IFactory = h.bind(undefined, 'i');
  export const iframe: IFactory = h.bind(undefined, 'iframe');
  export const img: IFactory = h.bind(undefined, 'img');
  export const input: IFactory = h.bind(undefined, 'input');
  export const ins: IFactory = h.bind(undefined, 'ins');
  export const kbd: IFactory = h.bind(undefined, 'kbd');
  export const label: IFactory = h.bind(undefined, 'label');
  export const legend: IFactory = h.bind(undefined, 'legend');
  export const li: IFactory = h.bind(undefined, 'li');
  export const main: IFactory = h.bind(undefined, 'main');
  export const map: IFactory = h.bind(undefined, 'map');
  export const mark: IFactory = h.bind(undefined, 'mark');
  export const meter: IFactory = h.bind(undefined, 'meter');
  export const nav: IFactory = h.bind(undefined, 'nav');
  export const noscript: IFactory = h.bind(undefined, 'noscript');
  export const object: IFactory = h.bind(undefined, 'object');
  export const ol: IFactory = h.bind(undefined, 'ol');
  export const optgroup: IFactory = h.bind(undefined, 'optgroup');
  export const option: IFactory = h.bind(undefined, 'option');
  export const output: IFactory = h.bind(undefined, 'output');
  export const p: IFactory = h.bind(undefined, 'p');
  export const param: IFactory = h.bind(undefined, 'param');
  export const pre: IFactory = h.bind(undefined, 'pre');
  export const progress: IFactory = h.bind(undefined, 'progress');
  export const q: IFactory = h.bind(undefined, 'q');
  export const rp: IFactory = h.bind(undefined, 'rp');
  export const rt: IFactory = h.bind(undefined, 'rt');
  export const ruby: IFactory = h.bind(undefined, 'ruby');
  export const s: IFactory = h.bind(undefined, 's');
  export const samp: IFactory = h.bind(undefined, 'samp');
  export const section: IFactory = h.bind(undefined, 'section');
  export const select: IFactory = h.bind(undefined, 'select');
  export const small: IFactory = h.bind(undefined, 'small');
  export const source: IFactory = h.bind(undefined, 'source');
  export const span: IFactory = h.bind(undefined, 'span');
  export const strong: IFactory = h.bind(undefined, 'strong');
  export const sub: IFactory = h.bind(undefined, 'sub');
  export const summary: IFactory = h.bind(undefined, 'summary');
  export const sup: IFactory = h.bind(undefined, 'sup');
  export const table: IFactory = h.bind(undefined, 'table');
  export const tbody: IFactory = h.bind(undefined, 'tbody');
  export const td: IFactory = h.bind(undefined, 'td');
  export const textarea: IFactory = h.bind(undefined, 'textarea');
  export const tfoot: IFactory = h.bind(undefined, 'tfoot');
  export const th: IFactory = h.bind(undefined, 'th');
  export const thead: IFactory = h.bind(undefined, 'thead');
  export const time: IFactory = h.bind(undefined, 'time');
  export const title: IFactory = h.bind(undefined, 'title');
  export const tr: IFactory = h.bind(undefined, 'tr');
  export const track: IFactory = h.bind(undefined, 'track');
  export const u: IFactory = h.bind(undefined, 'u');
  export const ul: IFactory = h.bind(undefined, 'ul');
  export const var_: IFactory = h.bind(undefined, 'var');
  export const video: IFactory = h.bind(undefined, 'video');
  export const wbr: IFactory = h.bind(undefined, 'wbr');
}

/**
 * DEPRECATED - pass the renderer arg to the h function instead
 *
 * Create a new "pass thru" virtual element node.
 *
 * @param tag - The tag name for the parent element.
 *
 * @param attrs - The attributes for the parent element, if any.
 *
 * @param renderer - an object with render and unrender functions, if any.
 *
 * @returns A new "pass thru" virtual element node for the given parameters.
 *
 */
export function hpass(
  tag: string,
  renderer?: VirtualElementPass.IRenderer
): VirtualElementPass;
export function hpass(
  tag: string,
  attrs: ElementAttrs,
  renderer?: VirtualElementPass.IRenderer
): VirtualElementPass;
export function hpass(tag: string): VirtualElementPass {
  let attrs: ElementAttrs = {};
  let renderer: VirtualElementPass.IRenderer | null = null;

  if (arguments.length === 2) {
    // eslint-disable-next-line prefer-rest-params
    const arg = arguments[1];

    if ('render' in arg) {
      renderer = arg;
    } else {
      attrs = arg;
    }
  } else if (arguments.length === 3) {
    // eslint-disable-next-line prefer-rest-params
    attrs = arguments[1];
    // eslint-disable-next-line prefer-rest-params
    renderer = arguments[2];
  } else if (arguments.length > 3) {
    throw new Error('hpass() should be called with 1, 2, or 3 arguments');
  }

  return new VirtualElementPass(tag, attrs, renderer);
}

/**
 * The namespace for the virtual DOM rendering functions.
 */
export namespace VirtualDOM {
  /**
   * Create a real DOM element from a virtual element node.
   *
   * @param node - The virtual element node to realize.
   *
   * @returns A new DOM element for the given virtual element node.
   *
   * #### Notes
   * This creates a brand new *real* DOM element with a structure which
   * matches the given virtual DOM node.
   *
   * If virtual diffing is desired, use the `render` function instead.
   */
  export function realize(node: VirtualText): Text;
  export function realize(node: VirtualElement): HTMLElement;
  export function realize(node: VirtualNode): HTMLElement | Text {
    return Private.createDOMNode(node);
  }

  /**
   * Render virtual DOM content into a host element.
   *
   * @param content - The virtual DOM content to render.
   *
   * @param host - The host element for the rendered content.
   *
   * #### Notes
   * This renders the delta from the previous rendering. It assumes that
   * the content of the host element is not manipulated by external code.
   *
   * Providing `null` content will clear the rendering.
   *
   * Externally modifying the provided content or the host element will
   * result in undefined rendering behavior.
   */
  export function render(
    content: VirtualNode | ReadonlyArray<VirtualNode> | null,
    host: HTMLElement
  ): void {
    let oldContent = Private.hostMap.get(host) || [];
    let newContent = Private.asContentArray(content);
    Private.hostMap.set(host, newContent);
    Private.updateContent(host, oldContent, newContent);
  }
}

/**
 * The namespace for the module implementation details.
 */
namespace Private {
  /**
   * A weak mapping of host element to virtual DOM content.
   */
  export const hostMap = new WeakMap<HTMLElement, ReadonlyArray<VirtualNode>>();

  /**
   * Cast a content value to a content array.
   */
  export function asContentArray(
    value: VirtualNode | ReadonlyArray<VirtualNode> | null
  ): ReadonlyArray<VirtualNode> {
    if (!value) {
      return [];
    }
    if (value instanceof Array) {
      return value as ReadonlyArray<VirtualNode>;
    }
    return [value as VirtualNode];
  }

  /**
   * Create a new DOM element for a virtual node.
   */
  export function createDOMNode(node: VirtualText): Text;
  export function createDOMNode(node: VirtualElement): HTMLElement;
  export function createDOMNode(node: VirtualNode): HTMLElement | Text;
  export function createDOMNode(
    node: VirtualNode,
    host: HTMLElement | null
  ): HTMLElement | Text;
  export function createDOMNode(
    node: VirtualNode,
    host: HTMLElement | null,
    before: Node | null
  ): HTMLElement | Text;
  export function createDOMNode(node: VirtualNode): HTMLElement | Text {
    // eslint-disable-next-line prefer-rest-params
    let host = arguments[1] || null;
    // eslint-disable-next-line prefer-rest-params
    const before = arguments[2] || null;

    if (host) {
      host.insertBefore(createDOMNode(node), before);
    } else {
      // Create a text node for a virtual text node.
      if (node.type === 'text') {
        return document.createTextNode(node.content);
      }

      // Create the HTML element with the specified tag.
      host = document.createElement(node.tag);

      // Add the attributes for the new element.
      addAttrs(host, node.attrs);

      if (node.renderer) {
        node.renderer.render(host, {
          attrs: node.attrs,
          children: node.children
        });
        return host;
      }

      // Recursively populate the element with child content.
      for (let i = 0, n = node.children.length; i < n; ++i) {
        createDOMNode(node.children[i], host);
      }
    }

    return host;
  }

  /**
   * Update a host element with the delta of the virtual content.
   *
   * This is the core "diff" algorithm. There is no explicit "patch"
   * phase. The host is patched at each step as the diff progresses.
   */
  export function updateContent(
    host: HTMLElement,
    oldContent: ReadonlyArray<VirtualNode>,
    newContent: ReadonlyArray<VirtualNode>
  ): void {
    // Bail early if the content is identical.
    if (oldContent === newContent) {
      return;
    }

    // Collect the old keyed elems into a mapping.
    let oldKeyed = collectKeys(host, oldContent);

    // Create a copy of the old content which can be modified in-place.
    let oldCopy = oldContent.slice();

    // Update the host with the new content. The diff always proceeds
    // forward and never modifies a previously visited index. The old
    // copy array is modified in-place to reflect the changes made to
    // the host children. This causes the stale nodes to be pushed to
    // the end of the host node and removed at the end of the loop.
    let currElem = host.firstChild;
    let newCount = newContent.length;
    for (let i = 0; i < newCount; ++i) {
      // If the old content is exhausted, create a new node.
      if (i >= oldCopy.length) {
        createDOMNode(newContent[i], host);
        continue;
      }

      // Lookup the old and new virtual nodes.
      let oldVNode = oldCopy[i];
      let newVNode = newContent[i];

      // If both elements are identical, there is nothing to do.
      if (oldVNode === newVNode) {
        currElem = currElem!.nextSibling;
        continue;
      }

      // Handle the simplest case of in-place text update first.
      if (oldVNode.type === 'text' && newVNode.type === 'text') {
        // Avoid spurious updates for performance.
        if (currElem!.textContent !== newVNode.content) {
          currElem!.textContent = newVNode.content;
        }
        currElem = currElem!.nextSibling;
        continue;
      }

      // If the old or new node is a text node, the other node is now
      // known to be an element node, so create and insert a new node.
      if (oldVNode.type === 'text' || newVNode.type === 'text') {
        ArrayExt.insert(oldCopy, i, newVNode);
        createDOMNode(newVNode, host, currElem);
        continue;
      }

      // If the old XOR new node has a custom renderer,
      // create and insert a new node.
      if (!oldVNode.renderer != !newVNode.renderer) {
        ArrayExt.insert(oldCopy, i, newVNode);
        createDOMNode(newVNode, host, currElem);
        continue;
      }

      // At this point, both nodes are known to be element nodes.

      // If the new elem is keyed, move an old keyed elem to the proper
      // location before proceeding with the diff. The search can start
      // at the current index, since the unmatched old keyed elems are
      // pushed forward in the old copy array.
      let newKey = newVNode.attrs.key;
      if (newKey && newKey in oldKeyed) {
        let pair = oldKeyed[newKey];
        if (pair.vNode !== oldVNode) {
          ArrayExt.move(oldCopy, oldCopy.indexOf(pair.vNode, i + 1), i);
          host.insertBefore(pair.element, currElem);
          oldVNode = pair.vNode;
          currElem = pair.element;
        }
      }

      // If both elements are identical, there is nothing to do.
      if (oldVNode === newVNode) {
        currElem = currElem!.nextSibling;
        continue;
      }

      // If the old elem is keyed and does not match the new elem key,
      // create a new node. This is necessary since the old keyed elem
      // may be matched at a later point in the diff.
      let oldKey = oldVNode.attrs.key;
      if (oldKey && oldKey !== newKey) {
        ArrayExt.insert(oldCopy, i, newVNode);
        createDOMNode(newVNode, host, currElem);
        continue;
      }

      // If the tags are different, create a new node.
      if (oldVNode.tag !== newVNode.tag) {
        ArrayExt.insert(oldCopy, i, newVNode);
        createDOMNode(newVNode, host, currElem);
        continue;
      }

      // At this point, the element can be updated in-place.

      // Update the element attributes.
      updateAttrs(currElem as HTMLElement, oldVNode.attrs, newVNode.attrs);

      // Update the element content.
      if (newVNode.renderer) {
        newVNode.renderer.render(currElem as HTMLElement, {
          attrs: newVNode.attrs,
          children: newVNode.children
        });
      } else {
        updateContent(
          currElem as HTMLElement,
          oldVNode.children,
          newVNode.children
        );
      }

      // Step to the next sibling element.
      currElem = currElem!.nextSibling;
    }

    // Cleanup stale DOM
    removeContent(host, oldCopy, newCount, true);
  }

  /**
   * Handle cleanup of stale vdom and its associated DOM. The host node is
   * traversed recursively (in depth-first order), and any explicit cleanup
   * required by a child node is carried out when it is visited (eg if a node
   * has a custom renderer, the renderer.unrender function will be called).
   * Once the subtree beneath each child of host has been completely visited,
   * that child will be removed via a call to host.removeChild.
   */
  function removeContent(
    host: HTMLElement,
    oldContent: ReadonlyArray<VirtualNode>,
    newCount: number,
    _sentinel: boolean
  ) {
    // Dispose of the old nodes pushed to the end of the host.
    for (let i = oldContent.length - 1; i >= newCount; --i) {
      const oldNode = oldContent[i];
      const child = (
        _sentinel ? host.lastChild : host.childNodes[i]
      ) as HTMLElement;

      // recursively clean up host children
      if (oldNode.type === 'text') {
        // pass
      } else if (oldNode.renderer && oldNode.renderer.unrender) {
        oldNode.renderer.unrender(child!, {
          attrs: oldNode.attrs,
          children: oldNode.children
        });
      } else {
        removeContent(child!, oldNode.children, 0, false);
      }

      if (_sentinel) {
        host.removeChild(child!);
      }
    }
  }

  /**
   * A set of special-cased attribute names.
   */
  const specialAttrs = {
    key: true,
    className: true,
    htmlFor: true,
    dataset: true,
    style: true
  };

  /**
   * Add element attributes to a newly created HTML element.
   */
  function addAttrs(element: HTMLElement, attrs: ElementAttrs): void {
    // Add the inline event listeners and node attributes.
    for (let name in attrs) {
      if (name in specialAttrs) {
        continue;
      }
      if (name.substr(0, 2) === 'on') {
        (element as any)[name] = (attrs as any)[name];
      } else {
        element.setAttribute(name, (attrs as any)[name]);
      }
    }

    // Add the element `class` attribute.
    if (attrs.className !== undefined) {
      element.setAttribute('class', attrs.className);
    }

    // Add the element `for` attribute.
    if (attrs.htmlFor !== undefined) {
      element.setAttribute('for', attrs.htmlFor);
    }

    // Add the dataset values.
    if (attrs.dataset) {
      addDataset(element, attrs.dataset);
    }

    // Add the inline styles.
    if (attrs.style) {
      addStyle(element, attrs.style);
    }
  }

  /**
   * Update the element attributes of an HTML element.
   */
  function updateAttrs(
    element: HTMLElement,
    oldAttrs: ElementAttrs,
    newAttrs: ElementAttrs
  ): void {
    // Do nothing if the attrs are the same object.
    if (oldAttrs === newAttrs) {
      return;
    }

    // Setup the strongly typed loop variable.
    let name: keyof ElementAttrs;

    // Remove attributes and listeners which no longer exist.
    for (name in oldAttrs) {
      if (name in specialAttrs || name in newAttrs) {
        continue;
      }
      if (name.substr(0, 2) === 'on') {
        (element as any)[name] = null;
      } else {
        element.removeAttribute(name);
      }
    }

    // Add and update new and existing attributes and listeners.
    for (name in newAttrs) {
      if (name in specialAttrs || oldAttrs[name] === newAttrs[name]) {
        continue;
      }
      if (name.substr(0, 2) === 'on') {
        (element as any)[name] = (newAttrs as any)[name];
      } else {
        element.setAttribute(name, (newAttrs as any)[name]);
      }
    }

    // Update the element `class` attribute.
    if (oldAttrs.className !== newAttrs.className) {
      if (newAttrs.className !== undefined) {
        element.setAttribute('class', newAttrs.className);
      } else {
        element.removeAttribute('class');
      }
    }

    // Add the element `for` attribute.
    if (oldAttrs.htmlFor !== newAttrs.htmlFor) {
      if (newAttrs.htmlFor !== undefined) {
        element.setAttribute('for', newAttrs.htmlFor);
      } else {
        element.removeAttribute('for');
      }
    }

    // Update the dataset values.
    if (oldAttrs.dataset !== newAttrs.dataset) {
      updateDataset(element, oldAttrs.dataset || {}, newAttrs.dataset || {});
    }

    // Update the inline styles.
    if (oldAttrs.style !== newAttrs.style) {
      updateStyle(element, oldAttrs.style || {}, newAttrs.style || {});
    }
  }

  /**
   * Add dataset values to a newly created HTML element.
   */
  function addDataset(element: HTMLElement, dataset: ElementDataset): void {
    for (let name in dataset) {
      element.setAttribute(`data-${name}`, dataset[name]);
    }
  }

  /**
   * Update the dataset values of an HTML element.
   */
  function updateDataset(
    element: HTMLElement,
    oldDataset: ElementDataset,
    newDataset: ElementDataset
  ): void {
    for (let name in oldDataset) {
      if (!(name in newDataset)) {
        element.removeAttribute(`data-${name}`);
      }
    }
    for (let name in newDataset) {
      if (oldDataset[name] !== newDataset[name]) {
        element.setAttribute(`data-${name}`, newDataset[name]);
      }
    }
  }

  /**
   * Add inline style values to a newly created HTML element.
   */
  function addStyle(element: HTMLElement, style: ElementInlineStyle): void {
    let elemStyle = element.style;
    let name: keyof ElementInlineStyle;
    for (name in style) {
      (elemStyle as any)[name] = style[name];
    }
  }

  /**
   * Update the inline style values of an HTML element.
   */
  function updateStyle(
    element: HTMLElement,
    oldStyle: ElementInlineStyle,
    newStyle: ElementInlineStyle
  ): void {
    let elemStyle = element.style;
    let name: keyof ElementInlineStyle;
    for (name in oldStyle) {
      if (!(name in newStyle)) {
        (elemStyle as any)[name] = '';
      }
    }
    for (name in newStyle) {
      if (oldStyle[name] !== newStyle[name]) {
        (elemStyle as any)[name] = newStyle[name];
      }
    }
  }

  /**
   * A mapping of string key to pair of element and rendered node.
   */
  type KeyMap = {
    [key: string]: { vNode: VirtualElement; element: HTMLElement };
  };

  /**
   * Collect a mapping of keyed elements for the host content.
   */
  function collectKeys(
    host: HTMLElement,
    content: ReadonlyArray<VirtualNode>
  ): KeyMap {
    let node = host.firstChild;
    let keyMap: KeyMap = Object.create(null);
    for (let vNode of content) {
      if (vNode.type === 'element' && vNode.attrs.key) {
        keyMap[vNode.attrs.key] = { vNode, element: node as HTMLElement };
      }
      node = node!.nextSibling;
    }
    return keyMap;
  }
}
