import {Deferred} from '#core/data-structures/promise';
import {rethrowAsync} from '#core/error';
import * as mode from '#core/mode';
import {map} from '#core/types/object';

/**
 * For type annotations where Element is a local variable.
 * @typedef {!Element}
 */
let ElementOrigDef;

/** @typedef {!typeof HTMLElement} */
let CustomElementConstructorDef;

/**
 * @typedef {{
 *  name: string,
 *  ctor: !CustomElementConstructorDef,
 * }}
 */
let CustomElementDef;

/**
 * Validates the custom element's name.
 * This intentionally ignores "valid" higher Unicode Code Points.
 * https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
 */
const VALID_NAME = /^[a-z][a-z0-9._]*-[a-z0-9._-]*$/;
const INVALID_NAMES = [
  'annotation-xml',
  'color-profile',
  'font-face',
  'font-face-src',
  'font-face-uri',
  'font-face-format',
  'font-face-name',
  'missing-glyph',
];

/**
 * A MutationObserverInit dictionary to track subtree modifications.
 */
const TRACK_SUBTREE = {
  'childList': true,
  'subtree': true,
};

/**
 * Asserts that the custom element name conforms to the spec.
 *
 * @param {!typeof SyntaxError} SyntaxError
 * @param {string} name
 */
function assertValidName(SyntaxError, name) {
  if (!VALID_NAME.test(name) || INVALID_NAMES.includes(name)) {
    throw new SyntaxError(`invalid custom element name "${name}"`);
  }
}

/**
 * Does win have a full Custom Elements registry?
 *
 * @param {!Window} win
 * @return {boolean}
 */
function hasCustomElements(win) {
  const {customElements} = win;

  return !!(
    customElements &&
    customElements.define &&
    customElements.get &&
    customElements.whenDefined
  );
}

/**
 * Was HTMLElement already patched for this window?
 *
 * @param {!Window} win
 * @return {boolean}
 */
function isPatched(win) {
  const tag = win.HTMLElement.toString();
  return tag.indexOf('[native code]') === -1;
}

/**
 * The public Custom Elements API.
 */
class CustomElementRegistry {
  /**
   * @param {!Window} win
   * @param {!Registry} registry
   */
  constructor(win, registry) {
    /** @const @private */
    this.win_ = win;

    /** @const @private */
    this.registry_ = registry;

    /** @private @const @type {!{[key: string]: !Deferred}} */
    this.pendingDefines_ = map();
  }

  /**
   * Register the custom element.
   *
   * @param {string} name
   * @param {!CustomElementConstructorDef} ctor
   * @param {!Object=} options
   */
  define(name, ctor, options) {
    this.registry_.define(name, ctor, options);

    // If anyone is waiting for this custom element to be defined, resolve
    // their promise.
    const pending = this.pendingDefines_;
    const deferred = pending[name];
    if (deferred) {
      deferred.resolve();
      delete pending[name];
    }
  }

  /**
   * Get the constructor of the (already defined) custom element.
   *
   * @param {string} name
   * @return {!CustomElementConstructorDef|undefined}
   */
  get(name) {
    const def = this.registry_.getByName(name);
    if (def) {
      return def.ctor;
    }
  }

  /**
   * Returns a promise that waits until the custom element is defined.
   * If the custom element is already defined, returns a resolved promise.
   *
   * @param {string} name
   * @return {!Promise<undefined>}
   */
  whenDefined(name) {
    const {Promise, SyntaxError} = this.win_;
    assertValidName(SyntaxError, name);

    if (this.registry_.getByName(name)) {
      return Promise.resolve();
    }

    const pending = this.pendingDefines_;
    let deferred = pending[name];
    if (!deferred) {
      deferred = new Deferred();
      pending[name] = deferred;
    }

    return deferred.promise;
  }

  /**
   * Upgrade all custom elements inside root.
   *
   * @param {!Node} root
   */
  upgrade(root) {
    this.registry_.upgrade(root);
  }
}

/**
 * This internal APIs necessary to run the CustomElementRegistry.
 * Since Registry is never exposed externally, all methods are actually
 * available on the instance.
 */
class Registry {
  /**
   * @param {!Window} win
   */
  constructor(win) {
    /** @private @const */
    this.win_ = win;

    /** @private @const @type {!{[key: string]: !CustomElementDef}} */
    this.definitions_ = map();

    /**
     * A up-to-date DOM selector for all custom elements.
     * @type {string}
     */
    this.query_ = '';

    /**
     * The currently upgrading element.
     * @private {?Element}
     */
    this.current_ = null;

    /**
     * Once started (after the first Custom Element definition), this tracks
     * DOM append and removals.
     *
     * @private {?MutationObserver}
     */
    this.mutationObserver_ = null;

    /**
     * All the observed DOM trees, including shadow trees.
     *
     * @private @const {!Array<!Node>}
     */
    this.roots_ = [win.document];
  }

  /**
   * The currently-being-upgraded custom element.
   *
   * When an already created (through the DOM parsing APIs, or innerHTML)
   * custom element node is being upgraded, we can't just create a new node
   * (it's illegal in the spec). But we still need to run the custom element's
   * constructor code on the node. We avoid this conundrum by running the
   * constructor while returning this current node in the HTMLElement
   * class constructor (the base class of all custom elements).
   *
   * @return {?Element}
   */
  current() {
    const current = this.current_;
    this.current_ = null;
    return current;
  }

  /**
   * Finds the custom element definition by name.
   *
   * @param {string} name
   * @return {!CustomElementDef|undefined}
   */
  getByName(name) {
    const definition = this.definitions_[name];
    if (definition) {
      return definition;
    }
  }

  /**
   * Finds the custom element definition by constructor instance.
   *
   * @param {!CustomElementConstructorDef} ctor
   * @return {!CustomElementDef|undefined}
   */
  getByConstructor(ctor) {
    const definitions = this.definitions_;

    for (const name in definitions) {
      const def = definitions[name];
      if (def.ctor === ctor) {
        return def;
      }
    }
  }

  /**
   * Registers the custom element definition, and upgrades all elements by that
   * name in the root document.
   *
   * @param {string} name
   * @param {!CustomElementConstructorDef} ctor
   * @param {!Object|undefined} options
   */
  define(name, ctor, options) {
    const {Error, SyntaxError} = this.win_;

    if (options) {
      throw new Error('Extending native custom elements is not supported');
    }

    assertValidName(SyntaxError, name);

    if (this.getByName(name) || this.getByConstructor(ctor)) {
      throw new Error(`duplicate definition "${name}"`);
    }

    // TODO(jridgewell): Record connectedCallback, disconnectedCallback,
    // adoptedCallback, attributeChangedCallback, and observedAttributes.
    // TODO(jridgewell): If attributeChangedCallback, gather observedAttributes
    this.definitions_[name] = {
      name,
      ctor,
    };

    this.observe_(name);
    for (const tree of this.roots_) {
      this.upgrade(tree, name);
    }
  }

  /**
   * Upgrades custom elements descendants of root (but not including root).
   *
   * When called with an opt_query, it both upgrades and connects the custom
   * elements (this is used during the custom element define algorithm).
   *
   * @param {!Node} root
   * @param {string=} opt_query
   */
  upgrade(root, opt_query) {
    // Only CustomElementRegistry.p.define provides a query (the newly defined
    // custom element). In this case, we are both upgrading _and_ connecting
    // the custom elements.
    const newlyDefined = !!opt_query;
    const query = opt_query || this.query_;
    const upgradeCandidates = this.queryAll_(root, query);

    for (const candidate of upgradeCandidates) {
      if (newlyDefined) {
        this.connectedCallback_(candidate);
      } else {
        this.upgradeSelf(candidate);
      }
    }
  }

  /**
   * Upgrades the custom element node, if a custom element has been registered
   * by this name.
   *
   * @param {!Node} node
   */
  upgradeSelf(node) {
    const def = this.getByName(node.localName);
    if (!def) {
      return;
    }

    this.upgradeSelf_(/** @type {!Element} */ (node), def);
  }

  /**
   * @param {!Node} root
   * @param {string} query
   * @return {!Array|!NodeList}
   */
  queryAll_(root, query) {
    if (!query || !root.querySelectorAll) {
      // Nothing to do...
      return [];
    }

    return root.querySelectorAll(query);
  }

  /**
   * Upgrades the (already created via DOM parsing) custom element.
   *
   * @param {!Element} node
   * @param {!CustomElementDef} def
   */
  upgradeSelf_(node, def) {
    const {ctor} = def;
    if (node instanceof ctor) {
      return;
    }

    // Despite how it looks, this is not a useless construction.
    // HTMLElementPolyfill (the base class of all custom elements) will return
    // the current node, allowing the custom element's subclass constructor to
    // run on the node. The node itself is already constructed, so the return
    // value is just the node.
    this.current_ = node;
    try {
      const el = new ctor();

      if (el !== node) {
        throw new this.win_.Error(
          'Constructor illegally returned a different instance.'
        );
      }
    } catch (e) {
      rethrowAsync(e);
    }
  }

  /**
   * Fires connectedCallback on the custom element, if it has one.
   * This also upgrades the custom element, since it may not have been
   * accessible via the root document before (a detached DOM tree).
   *
   * @param {!Node} node
   */
  connectedCallback_(node) {
    const def = this.getByName(node.localName);
    if (!def) {
      return;
    }
    node = /** @type {!HTMLElement} */ (node);
    this.upgradeSelf_(node, def);
    // TODO(jridgewell): It may be appropriate to adoptCallback, if the node
    // used to be in another doc.
    // TODO(jridgewell): I should be calling the definitions connectedCallback
    // with node as the context.
    if (node.connectedCallback) {
      try {
        node.connectedCallback();
      } catch (e) {
        rethrowAsync(e);
      }
    }
  }

  /**
   * Fires disconnectedCallback on the custom element, if it has one.
   *
   * @param {!Node} node
   */
  disconnectedCallback_(node) {
    // TODO(jridgewell): I should be calling the definitions connectedCallback
    // with node as the context.
    node = /** @type {!HTMLElement} */ (node);
    if (node.disconnectedCallback) {
      try {
        node.disconnectedCallback();
      } catch (e) {
        rethrowAsync(e);
      }
    }
  }

  /**
   * Records name as a registered custom element to observe.
   *
   * Starts the Mutation Observer if this is the first registered custom
   * element. This is deferred until the first custom element is defined to
   * speed up initial rendering of the page.
   *
   * Mutation Observers are conveniently available in every browser we care
   * about. When a node is connected to the root document, all custom
   * elements (including that node itself) will be upgraded and call
   * connectedCallback. When a node is disconnectedCallback from the root
   * document, all custom elements will call disconnectedCallback.
   *
   * @param {string} name
   */
  observe_(name) {
    if (this.query_) {
      this.query_ += `,${name}`;
      return;
    }

    this.query_ = name;

    // The first registered name starts the mutation observer.
    const mo = new this.win_.MutationObserver((records) => {
      if (records) {
        this.handleRecords_(records);
      }
    });
    this.mutationObserver_ = mo;

    // I would love to not have to hold onto all of the roots, since it's a
    // memory leak. Unfortunately, there's no way to iterate a list and hold
    // onto its contents weakly.
    for (const tree of this.roots_) {
      mo.observe(tree, TRACK_SUBTREE);
    }

    installPatches(this.win_, this);
  }

  /**
   * Adds the shadow tree to be observed by the polyfill.
   *
   * @param {!Node} tree
   */
  observe(tree) {
    this.roots_.push(tree);
    if (this.mutationObserver_) {
      this.mutationObserver_.observe(tree, TRACK_SUBTREE);
    }
  }

  /**
   * This causes a synchronous handling of all the Mutation Observer's tracked
   * mutations. This does nothing until the mutation observer is actually
   * registered on the first Custom Element definition.
   */
  sync() {
    if (this.mutationObserver_) {
      this.handleRecords_(this.mutationObserver_.takeRecords());
    }
  }

  /**
   * Handle all the Mutation Observer's Mutation Records.
   * All added custom elements will be upgraded (if not already) and call
   * connectedCallback. All removed custom elements will call
   * disconnectedCallback.
   *
   * @param {!Array<!MutationRecord>} records
   */
  handleRecords_(records) {
    for (const record of records) {
      if (!record) {
        continue;
      }

      const {addedNodes, removedNodes} = record;
      for (const node of addedNodes) {
        const connectedCandidates = this.queryAll_(node, this.query_);
        this.connectedCallback_(node);
        for (const candidate of connectedCandidates) {
          this.connectedCallback_(candidate);
        }
      }

      for (const node of removedNodes) {
        const disconnectedCandidates = this.queryAll_(node, this.query_);
        this.disconnectedCallback_(node);
        for (const candidate of disconnectedCandidates) {
          this.disconnectedCallback_(candidate);
        }
      }
    }
  }
}

/**
 * Patches the DOM APIs to support synchronous Custom Elements.
 * @param {!Window} win
 * @param {!Registry} registry
 */
function installPatches(win, registry) {
  const {Document, Element, Node, document} = win;
  const docProto = Document.prototype;
  const elProto = Element.prototype;
  const nodeProto = Node.prototype;
  const {createElement, importNode} = docProto;
  const {appendChild, cloneNode, insertBefore, removeChild, replaceChild} =
    nodeProto;

  // Patch createElement to immediately upgrade the custom element.
  // This has the added benefit that it avoids the "already created but needs
  // constructor code run" chicken-and-egg problem.
  docProto.createElement = function (name) {
    const def = registry.getByName(name);
    if (def) {
      return new def.ctor();
    }
    return createElement.apply(this, arguments);
  };

  // Patch importNode to immediately upgrade custom elements.
  // TODO(jridgewell): Can fire adoptedCallback for cross doc imports.
  docProto.importNode = function () {
    const imported = importNode.apply(this, arguments);

    // Only upgrade elements if the document that the nodes were imported into
    // is _this_ document. If it's another document, then that document's
    // element registry must do the upgrade.
    // Eg, when importing from a <template>, the cloned document fragment
    // should be upgraded. But importing from document into the <template>
    // should not.
    if (imported && this === document) {
      registry.upgradeSelf(imported);
      registry.upgrade(imported);
    }
    return imported;
  };

  // Patch appendChild to upgrade custom elements before returning.
  nodeProto.appendChild = function () {
    const appended = appendChild.apply(this, arguments);
    registry.sync();
    return appended;
  };

  // Patch insertBefore to upgrade custom elements before returning.
  nodeProto.insertBefore = function () {
    const inserted = insertBefore.apply(this, arguments);
    registry.sync();
    return inserted;
  };

  // Patch removeChild to upgrade custom elements before returning.
  nodeProto.removeChild = function () {
    const removed = removeChild.apply(this, arguments);
    registry.sync();
    return removed;
  };

  // Patch replaceChild to upgrade and detach custom elements before returning.
  nodeProto.replaceChild = function () {
    const replaced = replaceChild.apply(this, arguments);
    registry.sync();
    return replaced;
  };

  // Patch cloneNode to immediately upgrade custom elements.
  nodeProto.cloneNode = function () {
    const cloned = cloneNode.apply(this, arguments);

    // Only upgrade elements if the cloned node belonged to _this_ document.
    // Eg, when cloning a <template>'s content, the cloned document fragment
    // does not belong to this document.
    if (cloned.ownerDocument === document) {
      registry.upgradeSelf(cloned);
      registry.upgrade(cloned);
    }
    return cloned;
  };

  // Patch the innerHTML setter to immediately upgrade custom elements.
  // Note, this could technically fire connectedCallbacks if this node was
  // connected, but we leave that to the Mutation Observer.
  let innerHTMLProto = elProto;
  let innerHTMLDesc = Object.getOwnPropertyDescriptor(
    innerHTMLProto,
    'innerHTML'
  );
  if (!innerHTMLDesc) {
    // Sigh... IE11 puts innerHTML descriptor on HTMLElement. But, we've
    // replaced HTMLElement with a polyfill wrapper, so have to get its proto.
    innerHTMLProto = Object.getPrototypeOf(win.HTMLElement.prototype);
    innerHTMLDesc = Object.getOwnPropertyDescriptor(
      innerHTMLProto,
      'innerHTML'
    );
  }
  if (innerHTMLDesc?.configurable) {
    const innerHTMLSetter = innerHTMLDesc.set;
    innerHTMLDesc.set = function (html) {
      innerHTMLSetter.call(this, html);
      registry.upgrade(this);
    };
    Object.defineProperty(
      /** @type {!Object} */ (innerHTMLProto),
      'innerHTML',
      innerHTMLDesc
    );
  }
}

/**
 * Does the polyfilling.
 * @param {!Window} win
 */
function polyfill(win) {
  const {Element, HTMLElement, document} = win;
  const {createElement} = document;

  const registry = new Registry(win);
  const customElements = new CustomElementRegistry(win, registry);

  // Expose the custom element registry.
  // Object.getOwnPropertyDescriptor(window, 'customElements')
  // {get: ƒ, set: undefined, enumerable: true, configurable: true}
  Object.defineProperty(win, 'customElements', {
    enumerable: true,
    configurable: true,
    // writable: false,
    value: customElements,
  });

  // Have to patch shadow methods now, since there's no way to find shadow trees
  // later.
  const elProto = Element.prototype;
  const {attachShadow, createShadowRoot} = elProto;
  if (attachShadow) {
    /**
     * @param {{mode: string}} unused
     * @return {!ShadowRoot}
     */
    elProto.attachShadow = function (unused) {
      const shadow = attachShadow.apply(this, arguments);
      registry.observe(shadow);
      return shadow;
    };
    // Necessary for Shadow AMP
    elProto.attachShadow.toString = function () {
      return attachShadow.toString();
    };
  }
  if (createShadowRoot) {
    /** @return {!ShadowRoot} */
    elProto.createShadowRoot = function () {
      const shadow = createShadowRoot.apply(this, arguments);
      registry.observe(shadow);
      return shadow;
    };
    // Necessary for Shadow AMP
    elProto.createShadowRoot.toString = function () {
      return createShadowRoot.toString();
    };
  }

  /**
   * You can't use the real HTMLElement constructor, because you can't subclass
   * it without using native classes. So, mock its approximation using
   * createElement.
   * @return {!ElementOrigDef}
   */
  function HTMLElementPolyfill() {
    const {constructor} = this;

    // If we're upgrading an already created custom element, we can't create
    // another new node (by the spec, it must be the same node).
    let el = registry.current();

    // If there's not a already created custom element, we're being invoked via
    // `new`ing the constructor.
    //
    // Technically, we could get here via createElement, but we patched that.
    // If it the custom element was registered, the patch turned it into a
    // `new` call.
    // If it was not registered, the native createElement is used. And if
    // native createElement is being used and we got to this code, we're really
    // in an infinite loop (a native createElement call just below) so we've
    // got bigger problems.
    //
    // So just take my word we got here via `new`.
    if (!el) {
      // The custom element definition is an invariant. If the custom element
      // is registered, everything works. If it's not, it throws in the member
      // property access (only defined custom elements can be directly
      // constructed via `new`).
      const def = registry.getByConstructor(constructor);
      el = createElement.call(document, def.name);
    }

    // Finally, if the node was already constructed, we need to reset its
    // prototype to the custom element prototype. And if it wasn't already
    // constructed, we created a new node via native createElement, and we need
    // to reset its prototype. Basically always reset the prototype.
    setPrototypeOf(el, constructor.prototype);
    return el;
  }
  subClass(HTMLElement, HTMLElementPolyfill);

  // Expose the polyfilled HTMLElement constructor for everyone to extend from.
  win.HTMLElementOrig = win.HTMLElement;
  win.HTMLElement = HTMLElementPolyfill;

  // When we transpile `super` in Custom Element subclasses, we change it to
  // `superClass.call(this)` (where `superClass` is `HTMLElementPolyfill`).
  // That `.call` value is inherited from `Function.prototype`.
  // But, IE11's native HTMLElement hierarchy doesn't extend from Function!
  // And because `HTMLElementPolyfill` extends from `HTMLElement`, it doesn't
  // have a `.call`! So we need to manually install it.
  if (!HTMLElementPolyfill.call) {
    HTMLElementPolyfill.apply = win.Function.apply;
    HTMLElementPolyfill.bind = win.Function.bind;
    HTMLElementPolyfill.call = win.Function.call;
  }
}

/**
 * Wraps HTMLElement in a Reflect.construct constructor, so that transpiled
 * classes can `_this = superClass.call(this)` during their construction.
 *
 * This is only used when Custom Elements v1 is already available _and_ we're
 * using transpiled classes (which use ES5 construction idioms).
 *
 * @param {!Window} win
 * @suppress {globalThis}
 */
function wrapHTMLElement(win) {
  const {HTMLElement, Reflect} = win;
  /** @return {!Element} */
  function HTMLElementWrapper() {
    const ctor = /** @type {function(...?):?|undefined} */ (this.constructor);

    // Reflect.construct allows us to construct a new HTMLElement without using
    // `new` (which will always fail because native HTMLElement is a restricted
    // constructor).
    return Reflect.construct(HTMLElement, [], ctor);
  }
  subClass(HTMLElement, HTMLElementWrapper);

  // Expose the wrapped HTMLElement constructor for everyone to extend from.
  win.HTMLElementOrig = win.HTMLElement;
  win.HTMLElement = HTMLElementWrapper;
}

/**
 * Setups up prototype inheritance
 *
 * @param {!SUPER} superClass
 * @param {!SUB} subClass
 * @template SUPER
 * @template SUB
 */
function subClass(superClass, subClass) {
  // Object.getOwnPropertyDescriptor(superClass.prototype, 'constructor')
  // {value: ƒ, writable: true, enumerable: false, configurable: true}
  subClass.prototype = Object.create(superClass.prototype, {
    constructor: {
      // enumerable: false,
      configurable: true,
      writable: true,
      value: subClass,
    },
  });
  setPrototypeOf(subClass, superClass);
}

/**
 * Tests whether setting '__proto__' will change the prototype chain of an
 * object. Only needed for old IE.
 * @return {boolean}
 */
function supportsUnderProto() {
  const proto = {'test': true};
  const obj = {};
  obj.__proto__ = proto;
  return !!obj['test'];
}

/**
 * Sets the prototype chain of an object, with various fallbacks to support
 * old IE.
 * @param {!Object} obj
 * @param {!Object} prototype
 */
function setPrototypeOf(obj, prototype) {
  if (mode.isEsm() || Object.setPrototypeOf) {
    // Every decent browser.
    Object.setPrototypeOf(obj, prototype);
  } else if (supportsUnderProto()) {
    // IE11
    obj.__proto__ = prototype;
  } else {
    // IE10 man. :sigh:
    copyProperties(obj, prototype);
  }
}

/**
 * Copies the property descriptors from prototype to obj. This is only
 * necessary for old IE, which can't properly set the prototype of an already
 * created object.
 * @param {!Object} obj
 * @param {!Object} prototype
 * @visibleForTesting
 */
export function copyProperties(obj, prototype) {
  let current = prototype;
  while (current !== null) {
    if (Object.isPrototypeOf.call(current, obj)) {
      break;
    }

    for (const prop of Object.getOwnPropertyNames(current)) {
      if (Object.hasOwnProperty.call(obj, prop)) {
        continue;
      }

      const desc = /** @type {!ObjectPropertyDescriptor<Object>} */ (
        Object.getOwnPropertyDescriptor(current, prop)
      );
      Object.defineProperty(obj, prop, desc);
    }

    current = Object.getPrototypeOf(current);
  }
}

/**
 * Polyfills Custom Elements v1 API. This has 5 modes:
 *
 * 1. Custom elements v1 already supported, using native classes
 * 2. Custom elements v1 already supported, using transpiled classes
 * 3. Custom elements v1 not supported, using native classes
 * 4. Custom elements v1 not supported, using transpiled classes
 * 5. No sample class constructor provided
 *
 * In mode 1, nothing is done. In mode 2, a minimal polyfill is used to support
 * extending the HTMLElement base class. In mode 3, 4, and 5 a full polyfill is
 * done.
 *
 * @param {!Window} win
 * @param {!Function=} ctor
 */
export function install(win, ctor) {
  // Don't install in no-DOM environments e.g. worker.
  const shouldInstall = win.document;
  const hasCE = hasCustomElements(win);
  if (!shouldInstall || (hasCE && isPatched(win))) {
    return;
  }

  let install = true;
  let installWrapper = false;

  if (ctor && hasCE) {
    // If ctor is constructable without new, it's a function. That means it was
    // compiled down, and we need to do the minimal polyfill because all you
    // cannot extend HTMLElement without native classes.
    try {
      const {Reflect} = win;

      // "Construct" ctor using ES5 idioms
      // I'm not sure why, but Closure will complain at the
      // `Function.call.call()` below unless we cast to a Function instance
      // here.
      const instance = /** @type {!Function} */ (Object.create(ctor.prototype));

      // This will throw an error unless we're in a transpiled environment.
      // Native classes must be called as `new Ctor`, not `Ctor.call(instance)`.
      // We use `Function.call.call` because Closure is too smart for regular
      // `Ctor.call`.
      Function.call.call(ctor, instance);

      // If that didn't throw, we're transpiled.
      // Let's find out if we can wrap HTMLElement and avoid a full patch.
      installWrapper = !!Reflect?.construct;
    } catch (e) {
      // The ctor threw when we constructed it via ES5, so it's a real class.
      // We're ok to not install the polyfill.
      install = false;
    }
  }

  if (installWrapper) {
    wrapHTMLElement(win);
  } else if (install) {
    polyfill(win);
  }
}
