text: /**
   * Copyright 2015 The AMP HTML Authors. All Rights Reserved.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS-IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  import {
    UPGRADE_TO_CUSTOMELEMENT_PROMISE,
    UPGRADE_TO_CUSTOMELEMENT_RESOLVER,
  } from './amp-element-helpers';
  import {startupChunk} from './chunk';
  import {shouldBlockOnConsentByMeta} from './consent';
  import {AmpEvents} from './core/constants/amp-events';
  import {CommonSignals} from './core/constants/common-signals';
  import {ReadyState} from './core/constants/ready-state';
  import {tryResolve} from './core/data-structures/promise';
  import {Signals} from './core/data-structures/signals';
  import * as dom from './core/dom';
  import {Layout, LayoutPriority, isLoadingAllowed} from './core/dom/layout';
  import {MediaQueryProps} from './core/dom/media-query-props';
  import * as query from './core/dom/query';
  import {setStyle} from './core/dom/style';
  import {rethrowAsync} from './core/error';
  import {toWin} from './core/window';
  import {ElementStub} from './element-stub';
  import {
    blockedByConsentError,
    cancellation,
    isBlockedByConsent,
    isCancellation,
    reportError,
  } from './error-reporting';
  import {isExperimentOn} from './experiments';
  import {dev, devAssert, user, userAssert} from './log';
  import {getMode} from './mode';
  import {Services} from './service';
  import {ResourceState} from './service/resource';
  import {getSchedulerForDoc} from './service/scheduler';
  import {applyStaticLayout} from './static-layout';
  import {getIntersectionChangeEntry} from './utils/intersection-observer-3p-host';
  
  const TAG = 'CustomElement';
  
  /**
   * @enum {number}
   */
  const UpgradeState = {
    NOT_UPGRADED: 1,
    UPGRADED: 2,
    UPGRADE_FAILED: 3,
    UPGRADE_IN_PROGRESS: 4,
  };
  
  const NO_BUBBLES = {bubbles: false};
  const RETURN_TRUE = () => true;
  
  /**
   * Caches whether the template tag is supported to avoid memory allocations.
   * @type {boolean|undefined}
   */
  let templateTagSupported;
  
  /** @type {!Array} */
  export const stubbedElements = [];
  
  /**
   * Whether this platform supports template tags.
   * @return {boolean}
   */
  function isTemplateTagSupported() {
    if (templateTagSupported === undefined) {
      const template = self.document.createElement('template');
      templateTagSupported = 'content' in template;
    }
    return templateTagSupported;
  }
  
  /**
   * Creates a named custom element class.
   *
   * @param {!Window} win The window in which to register the custom element.
   * @param {function(!./service/ampdoc-impl.AmpDoc, !AmpElement, ?(typeof BaseElement))} elementConnectedCallback
   * @return {typeof AmpElement} The custom element class.
   */
  export function createCustomElementClass(win, elementConnectedCallback) {
    const BaseCustomElement = /** @type {typeof HTMLElement} */ (
      createBaseCustomElementClass(win, elementConnectedCallback)
    );
    // It's necessary to create a subclass, because the same "base" class cannot
    // be registered to multiple custom elements.
    class CustomAmpElement extends BaseCustomElement {
      /**
       * adoptedCallback is only called when using a Native implementation of Custom Elements V1.
       * Our polyfill does not call this method.
       */
      adoptedCallback() {
        // Work around an issue with Firefox changing the prototype of our
        // already constructed element to the new document's HTMLElement.
        if (Object.getPrototypeOf(this) !== customAmpElementProto) {
          Object.setPrototypeOf(this, customAmpElementProto);
        }
      }
    }
    const customAmpElementProto = CustomAmpElement.prototype;
    return /** @type {typeof AmpElement} */ (CustomAmpElement);
  }
  
  /**
   * Creates a base custom element class.
   *
   * @param {!Window} win The window in which to register the custom element.
   * @param {function(!./service/ampdoc-impl.AmpDoc, !AmpElement, ?(typeof BaseElement))} elementConnectedCallback
   * @return {typeof HTMLElement}
   */
  function createBaseCustomElementClass(win, elementConnectedCallback) {
    if (win.__AMP_BASE_CE_CLASS) {
      return win.__AMP_BASE_CE_CLASS;
    }
    const htmlElement = /** @type {typeof HTMLElement} */ (win.HTMLElement);
  
    /**
     * @abstract @extends {HTMLElement}
     */
    class BaseCustomElement extends htmlElement {
      /** */
      constructor() {
        super();
        this.createdCallback();
      }
  
      /**
       * Called when elements is created. Sets instance vars since there is no
       * constructor.
       * @final
       */
      createdCallback() {
        // Flag "notbuilt" is removed by Resource manager when the resource is
        // considered to be built. See "setBuilt" method.
        /** @private {boolean} */
        this.built_ = false;
  
        /**
         * Several APIs require the element to be connected to the DOM tree, but
         * the CustomElement lifecycle APIs are async. This lead to subtle bugs
         * that require state tracking. See #12849, https://crbug.com/821195, and
         * https://bugs.webkit.org/show_bug.cgi?id=180940.
         * @private {boolean}
         */
        this.isConnected_ = false;
  
        /** @private {?Promise} */
        this.buildingPromise_ = null;
  
        /**
         * Indicates that the `mountCallback()` has been called and it hasn't
         * been reversed with an `unmountCallback()` call.
         * @private {boolean}
         */
        this.mounted_ = false;
  
        /** @private {?Promise} */
        this.mountPromise_ = null;
  
        /** @private {?AbortController} */
        this.mountAbortController_ = null;
  
        /** @private {!ReadyState} */
        this.readyState_ = ReadyState.UPGRADING;
  
        /** @type {boolean} */
        this.everAttached = false;
  
        /**
         * Ampdoc can only be looked up when an element is attached.
         * @private {?./service/ampdoc-impl.AmpDoc}
         */
        this.ampdoc_ = null;
  
        /**
         * Resources can only be looked up when an element is attached.
         * @private {?./service/resources-interface.ResourcesInterface}
         */
        this.resources_ = null;
  
        /** @private {!Layout} */
        this.layout_ = Layout.NODISPLAY;
  
        /** @private {number} */
        this.layoutCount_ = 0;
  
        /** @private {boolean} */
        this.isFirstLayoutCompleted_ = false;
  
        /** @public {boolean} */
        this.warnOnMissingOverflow = true;
  
        /**
         * This element can be assigned by the {@link applyStaticLayout} to a
         * child element that will be used to size this element.
         * @package {?Element|undefined}
         */
        this.sizerElement = undefined;
  
        /** @private {?Element|undefined} */
        this.overflowElement_ = undefined;
  
        /**
         * The time at which this element was scheduled for layout relative to
         * the epoch. This value will be set to 0 until the this element has been
         * scheduled.
         * Note that this value may change over time if the element is enqueued,
         * then dequeued and re-enqueued by the scheduler.
         * @type {number|undefined}
         */
        this.layoutScheduleTime = undefined;
  
        // Closure compiler appears to mark HTMLElement as @struct which
        // disables bracket access. Force this with a type coercion.
        const nonStructThis = /** @type {!Object} */ (this);
  
        // `opt_implementationClass` is only used for tests.
        /** @type {?(typeof ../base-element.BaseElement)} */
        let Ctor =
          win.__AMP_EXTENDED_ELEMENTS &&
          win.__AMP_EXTENDED_ELEMENTS[this.localName];
        if (getMode().test && nonStructThis['implementationClassForTesting']) {
          Ctor = nonStructThis['implementationClassForTesting'];
        }
  
        /** @private {?(typeof ../base-element.BaseElement)} */
        this.implClass_ = Ctor === ElementStub ? null : Ctor || null;
  
        if (!this.implClass_) {
          stubbedElements.push(this);
        }
  
        /** @private {?./base-element.BaseElement} */
        this.impl_ = null;
  
        /**
         * An element always starts in a unupgraded state until it's added to DOM
         * for the first time in which case it can be upgraded immediately or wait
         * for script download or `upgradeCallback`.
         * @private {!UpgradeState}
         */
        this.upgradeState_ = UpgradeState.NOT_UPGRADED;
  
        /**
         * Time delay imposed by baseElement upgradeCallback.  If no
         * upgradeCallback specified or not yet executed, delay is 0.
         * @private {number}
         */
        this.upgradeDelayMs_ = 0;
  
        /**
         * Action queue is initially created and kept around until the element
         * is ready to send actions directly to the implementation.
         * - undefined initially
         * - array if used
         * - null after unspun
         * @private {?Array<!./service/action-impl.ActionInvocation>|undefined}
         */
        this.actionQueue_ = undefined;
  
        /**
         * Whether the element is in the template.
         * @private {boolean|undefined}
         */
        this.isInTemplate_ = undefined;
  
        /** @private @const */
        this.signals_ = new Signals();
  
        if (this.implClass_) {
          this.signals_.signal(CommonSignals.READY_TO_UPGRADE);
        }
  
        const perf = Services.performanceForOrNull(win);
        /** @private {boolean} */
        this.perfOn_ = perf && perf.isPerformanceTrackingOn();
  
        /** @private {?MediaQueryProps} */
        this.mediaQueryProps_ = null;
  
        if (nonStructThis[UPGRADE_TO_CUSTOMELEMENT_RESOLVER]) {
          nonStructThis[UPGRADE_TO_CUSTOMELEMENT_RESOLVER](nonStructThis);
          delete nonStructThis[UPGRADE_TO_CUSTOMELEMENT_RESOLVER];
          delete nonStructThis[UPGRADE_TO_CUSTOMELEMENT_PROMISE];
        }
      }
  
      /** @return {!ReadyState} */
      get readyState() {
        return this.readyState_;
      }
  
      /** @return {!Signals} */
      signals() {
        return this.signals_;
      }
  
      /**
       * Returns the associated ampdoc. Only available after attachment. It throws
       * exception before the element is attached.
       * @return {!./service/ampdoc-impl.AmpDoc}
       * @final
       * @package
       */
      getAmpDoc() {
        devAssert(this.ampdoc_, 'no ampdoc yet, since element is not attached');
        return /** @type {!./service/ampdoc-impl.AmpDoc} */ (this.ampdoc_);
      }
  
      /**
       * Returns Resources manager. Only available after attachment. It throws
       * exception before the element is attached.
       * @return {!./service/resources-interface.ResourcesInterface}
       * @final
       * @package
       */
      getResources() {
        devAssert(
          this.resources_,
          'no resources yet, since element is not attached'
        );
        return /** @type {!./service/resources-interface.ResourcesInterface} */ (
          this.resources_
        );
      }
  
      /**
       * Whether the element has been upgraded yet. Always returns false when
       * the element has not yet been added to DOM. After the element has been
       * added to DOM, the value depends on the `BaseElement` implementation and
       * its `upgradeElement` callback.
       * @return {boolean}
       * @final
       */
      isUpgraded() {
        return this.upgradeState_ == UpgradeState.UPGRADED;
      }
  
      /** @return {!Promise} */
      whenUpgraded() {
        return this.signals_.whenSignal(CommonSignals.UPGRADED);
      }
  
      /**
       * Upgrades the element to the provided new implementation. If element
       * has already been attached, it's layout validation and attachment flows
       * are repeated for the new implementation.
       * @param {typeof ./base-element.BaseElement} newImplClass
       * @final @package
       */
      upgrade(newImplClass) {
        if (this.isInTemplate_) {
          return;
        }
        if (this.upgradeState_ != UpgradeState.NOT_UPGRADED) {
          // Already upgraded or in progress or failed.
          return;
        }
  
        this.implClass_ = newImplClass;
        this.signals_.signal(CommonSignals.READY_TO_UPGRADE);
        if (this.everAttached) {
          // Usually, we do an implementation upgrade when the element is
          // attached to the DOM. But, if it hadn't yet upgraded from
          // ElementStub, we couldn't. Now that it's upgraded from a stub, go
          // ahead and do the full upgrade.
          this.upgradeOrSchedule_();
        }
      }
  
      /**
       * Time delay imposed by baseElement upgradeCallback.  If no
       * upgradeCallback specified or not yet executed, delay is 0.
       * @return {number}
       */
      getUpgradeDelayMs() {
        return this.upgradeDelayMs_;
      }
  
      /**
       * Completes the upgrade of the element with the provided implementation.
       * @param {!./base-element.BaseElement} newImpl
       * @param {number} upgradeStartTime
       * @final @private
       */
      completeUpgrade_(newImpl, upgradeStartTime) {
        this.impl_ = newImpl;
        this.upgradeDelayMs_ = win.Date.now() - upgradeStartTime;
        this.upgradeState_ = UpgradeState.UPGRADED;
        this.setReadyStateInternal(ReadyState.BUILDING);
        this.classList.remove('amp-unresolved');
        this.classList.remove('i-amphtml-unresolved');
        this.assertLayout_();
        this.dispatchCustomEventForTesting(AmpEvents.ATTACHED);
        if (!this.R1()) {
          this.getResources().upgraded(this);
        }
        this.signals_.signal(CommonSignals.UPGRADED);
      }
  
      /** @private */
      assertLayout_() {
        if (
          this.layout_ != Layout.NODISPLAY &&
          this.impl_ &&
          !this.impl_.isLayoutSupported(this.layout_)
        ) {
          userAssert(
            this.getAttribute('layout'),
            'The element did not specify a layout attribute. ' +
              'Check https://amp.dev/documentation/guides-and-tutorials/' +
              'develop/style_and_layout/control_layout and the respective ' +
              'element documentation for details.'
          );
          userAssert(false, `Layout not supported: ${this.layout_}`);
        }
      }
  
      /**
       * Get the priority to build the element.
       * @return {number}
       */
      getBuildPriority() {
        return this.implClass_
          ? this.implClass_.getBuildPriority(this)
          : LayoutPriority.BACKGROUND;
      }
  
      /**
       * Get the priority to load the element.
       * @return {number}
       * TODO(#31915): remove once R1 migration is complete.
       */
      getLayoutPriority() {
        return this.impl_
          ? this.impl_.getLayoutPriority()
          : LayoutPriority.BACKGROUND;
      }
  
      /**
       * Get the default action alias.
       * @return {?string}
       */
      getDefaultActionAlias() {
        devAssert(
          this.isUpgraded(),
          'Cannot get default action alias of unupgraded element'
        );
        return this.impl_.getDefaultActionAlias();
      }
  
      /** @return {boolean} */
      isBuilding() {
        return !!this.buildingPromise_;
      }
  
      /**
       * Whether the element has been built. A built element had its
       * {@link buildCallback} method successfully invoked.
       * @return {boolean}
       * @final
       */
      isBuilt() {
        return this.built_;
      }
  
      /**
       * Returns the promise that's resolved when the element has been built. If
       * the build fails, the resulting promise is rejected.
       * @return {!Promise}
       */
      whenBuilt() {
        return this.signals_.whenSignal(CommonSignals.BUILT);
      }
  
      /**
       * Requests or requires the element to be built. The build is done by
       * invoking {@link BaseElement.buildCallback} method.
       *
       * Can only be called on a upgraded element. May only be called from
       * resource.js to ensure an element and its resource are in sync.
       *
       * @return {?Promise}
       * @final
       * @restricted
       */
      buildInternal() {
        assertNotTemplate(this);
        devAssert(this.implClass_, 'Cannot build unupgraded element');
        if (this.buildingPromise_) {
          return this.buildingPromise_;
        }
  
        this.setReadyStateInternal(ReadyState.BUILDING);
  
        // Create the instance.
        const implPromise = this.createImpl_();
  
        // Wait for consent.
        const consentPromise = implPromise.then(() => {
          const policyId = this.getConsentPolicy_();
          const isGranularConsentExperimentOn = isExperimentOn(
            win,
            'amp-consent-granular-consent'
          );
          const purposeConsents =
            isGranularConsentExperimentOn && !policyId
              ? this.getPurposesConsent_()
              : null;
  
          if (!policyId && !(isGranularConsentExperimentOn && purposeConsents)) {
            return;
          }
          // Must have policyId or granularExp w/ purposeConsents
          return Services.consentPolicyServiceForDocOrNull(this)
            .then((policy) => {
              if (!policy) {
                return true;
              }
              return policyId
                ? policy.whenPolicyUnblock(policyId)
                : policy.whenPurposesUnblock(purposeConsents);
            })
            .then((shouldUnblock) => {
              if (!shouldUnblock) {
                throw blockedByConsentError();
              }
            });
        });
  
        // Build callback.
        const buildPromise = consentPromise.then(() =>
          devAssert(this.impl_).buildCallback()
        );
  
        // Build the element.
        return (this.buildingPromise_ = buildPromise.then(
          () => {
            this.built_ = true;
            this.classList.add('i-amphtml-built');
            this.classList.remove('i-amphtml-notbuilt');
            this.classList.remove('amp-notbuilt');
            this.signals_.signal(CommonSignals.BUILT);
  
            if (this.R1()) {
              this.setReadyStateInternal(
                this.readyState_ != ReadyState.BUILDING
                  ? this.readyState_
                  : ReadyState.MOUNTING
              );
            } else {
              this.setReadyStateInternal(ReadyState.LOADING);
              this.preconnect(/* onLayout */ false);
            }
  
            if (this.isConnected_) {
              this.connected_();
            }
  
            if (this.actionQueue_) {
              // Only schedule when the queue is not empty, which should be
              // the case 99% of the time.
              Services.timerFor(toWin(this.ownerDocument.defaultView)).delay(
                this.dequeueActions_.bind(this),
                1
              );
            }
            if (!this.getPlaceholder()) {
              const placeholder = this.createPlaceholder();
              if (placeholder) {
                this.appendChild(placeholder);
              }
            }
          },
          (reason) => {
            this.signals_.rejectSignal(
              CommonSignals.BUILT,
              /** @type {!Error} */ (reason)
            );
  
            if (this.R1()) {
              this.setReadyStateInternal(ReadyState.ERROR, reason);
            }
  
            if (!isBlockedByConsent(reason)) {
              reportError(reason, this);
            }
            throw reason;
          }
        ));
      }
  
      /**
       * @return {!Promise}
       */
      build() {
        if (this.buildingPromise_) {
          return this.buildingPromise_;
        }
  
        const readyPromise = this.signals_.whenSignal(
          CommonSignals.READY_TO_UPGRADE
        );
        return readyPromise.then(() => {
          if (this.R1()) {
            const scheduler = getSchedulerForDoc(this.getAmpDoc());
            scheduler.scheduleAsap(this);
          }
          return this.whenBuilt();
        });
      }
  
      /**
       * Mounts the element by calling the `BaseElement.mountCallback` method.
       *
       * Can only be called on a upgraded element. May only be called from
       * scheduler.js.
       *
       * @return {!Promise}
       * @final
       * @restricted
       */
      mountInternal() {
        if (this.mountPromise_) {
          return this.mountPromise_;
        }
        this.mountAbortController_ =
          this.mountAbortController_ || new AbortController();
        const {signal} = this.mountAbortController_;
        return (this.mountPromise_ = this.buildInternal()
          .then(() => {
            devAssert(this.R1());
            if (signal.aborted) {
              // Mounting has been canceled.
              return;
            }
            this.setReadyStateInternal(
              this.readyState_ != ReadyState.MOUNTING
                ? this.readyState_
                : this.implClass_.usesLoading(this)
                ? ReadyState.LOADING
                : ReadyState.MOUNTING
            );
            this.mounted_ = true;
            const result = this.impl_.mountCallback(signal);
            // The promise supports the V0 format for easy migration. If the
            // `mountCallback` returns a promise, the assumption is that the
            // element has finished loading when the promise completes.
            return result ? result.then(RETURN_TRUE) : false;
          })
          .then((hasLoaded) => {
            this.mountAbortController_ = null;
            if (signal.aborted) {
              throw cancellation();
            }
            this.signals_.signal(CommonSignals.MOUNTED);
            if (!this.implClass_.usesLoading(this) || hasLoaded) {
              this.setReadyStateInternal(ReadyState.COMPLETE);
            }
          })
          .catch((reason) => {
            this.mountAbortController_ = null;
            if (isCancellation(reason)) {
              this.mountPromise_ = null;
            } else {
              this.signals_.rejectSignal(
                CommonSignals.MOUNTED,
                /** @type {!Error} */ (reason)
              );
              this.setReadyStateInternal(ReadyState.ERROR, reason);
            }
            throw reason;
          }));
      }
  
      /**
       * Requests the element to be mounted as soon as possible.
       * @return {!Promise}
       * @final
       */
      mount() {
        if (this.mountPromise_) {
          return this.mountPromise_;
        }
  
        // Create the abort controller right away to ensure that we the unmount
        // will properly cancel this operation.
        this.mountAbortController_ =
          this.mountAbortController_ || new AbortController();
        const {signal} = this.mountAbortController_;
  
        const readyPromise = this.signals_.whenSignal(
          CommonSignals.READY_TO_UPGRADE
        );
        return readyPromise.then(() => {
          if (!this.R1()) {
            return this.whenBuilt();
          }
          if (signal.aborted) {
            throw cancellation();
          }
          const scheduler = getSchedulerForDoc(this.getAmpDoc());
          scheduler.scheduleAsap(this);
          return this.whenMounted();
        });
      }
  
      /**
       * Unmounts the element and makes it ready for the next mounting
       * operation.
       * @final
       */
      unmount() {
        // Ensure that the element is paused.
        if (this.isConnected_) {
          this.pause();
        }
  
        // Legacy R0 elements simply unlayout.
        if (!this.R1()) {
          this.unlayout_();
          return;
        }
  
        // Cancel the currently mounting operation.
        if (this.mountAbortController_) {
          this.mountAbortController_.abort();
          this.mountAbortController_ = null;
        }
  
        // Unschedule a currently pending mount request.
        const scheduler = getSchedulerForDoc(this.getAmpDoc());
        scheduler.unschedule(this);
  
        // Try to unmount if the element has been built already.
        if (this.mounted_) {
          this.impl_.unmountCallback();
        }
  
        // Complete unmount and reset the state.
        this.mounted_ = false;
        this.mountPromise_ = null;
        this.reset_();
  
        // Prepare for the next mount if the element is connected.
        if (this.isConnected_) {
          this.upgradeOrSchedule_(/* opt_disablePreload */ true);
        }
      }
  
      /**
       * Returns the promise that's resolved when the element has been mounted. If
       * the mount fails, the resulting promise is rejected.
       * @return {!Promise}
       */
      whenMounted() {
        return this.signals_.whenSignal(CommonSignals.MOUNTED);
      }
  
      /**
       * @return {!Promise}
       * @final
       */
      whenLoaded() {
        return this.signals_.whenSignal(CommonSignals.LOAD_END);
      }
  
      /**
       * Ensure that the element is eagerly loaded.
       *
       * @param {number=} opt_parentPriority
       * @return {!Promise}
       * @final
       */
      ensureLoaded(opt_parentPriority) {
        return this.mount().then(() => {
          if (this.R1()) {
            if (this.implClass_.usesLoading(this)) {
              this.impl_.ensureLoaded();
            }
            return this.whenLoaded();
          }
  
          // Very ugly! The "built" signal must be resolved from the Resource
          // and not the element itself because the Resource has not correctly
          // set its state for the downstream to process it correctly.
          const resource = this.getResource_();
          return resource.whenBuilt().then(() => {
            if (resource.getState() == ResourceState.LAYOUT_COMPLETE) {
              return;
            }
            if (
              resource.getState() != ResourceState.LAYOUT_SCHEDULED ||
              resource.isMeasureRequested()
            ) {
              resource.measure();
            }
            if (!resource.isDisplayed()) {
              return;
            }
            this.getResources().scheduleLayoutOrPreload(
              resource,
              /* layout */ true,
              opt_parentPriority,
              /* forceOutsideViewport */ true
            );
            return this.whenLoaded();
          });
        });
      }
  
      /**
       * See `BaseElement.setAsContainer`.
       *
       * @param {!Element=} opt_scroller A child of the container that should be
       * monitored. Typically a scrollable element.
       * @restricted
       * @final
       */
      setAsContainerInternal(opt_scroller) {
        const builder = getSchedulerForDoc(this.getAmpDoc());
        builder.setContainer(this, opt_scroller);
      }
  
      /**
       * See `BaseElement.removeAsContainer`.
       * @restricted
       * @final
       */
      removeAsContainerInternal() {
        const builder = getSchedulerForDoc(this.getAmpDoc());
        builder.removeContainer(this);
      }
  
      /**
       * Update the internal ready state.
       *
       * @param {!ReadyState} state
       * @param {*=} opt_failure
       * @protected
       * @final
       */
      setReadyStateInternal(state, opt_failure) {
        if (state === this.readyState_) {
          return;
        }
  
        this.readyState_ = state;
  
        if (!this.R1()) {
          return;
        }
  
        switch (state) {
          case ReadyState.LOADING:
            this.signals_.signal(CommonSignals.LOAD_START);
            this.signals_.reset(CommonSignals.UNLOAD);
            this.signals_.reset(CommonSignals.LOAD_END);
            this.classList.add('i-amphtml-layout');
            // Potentially start the loading indicator.
            this.toggleLoading(true);
            this.dispatchCustomEventForTesting(AmpEvents.LOAD_START);
            return;
          case ReadyState.COMPLETE:
            // LOAD_START is set just in case. It won't be overwritten if
            // it had been set before.
            this.signals_.signal(CommonSignals.LOAD_START);
            this.signals_.signal(CommonSignals.LOAD_END);
            this.signals_.reset(CommonSignals.UNLOAD);
            this.classList.add('i-amphtml-layout');
            this.toggleLoading(false);
            dom.dispatchCustomEvent(this, 'load', null, NO_BUBBLES);
            this.dispatchCustomEventForTesting(AmpEvents.LOAD_END);
            return;
          case ReadyState.ERROR:
            this.signals_.rejectSignal(
              CommonSignals.LOAD_END,
              /** @type {!Error} */ (opt_failure)
            );
            this.toggleLoading(false);
            dom.dispatchCustomEvent(this, 'error', opt_failure, NO_BUBBLES);
            return;
        }
      }
  
      /**
       * Called to instruct the element to preconnect to hosts it uses during
       * layout.
       * @param {boolean} onLayout Whether this was called after a layout.
       * TODO(#31915): remove once R1 migration is complete.
       */
      preconnect(onLayout) {
        devAssert(this.isUpgraded());
        if (onLayout) {
          this.impl_.preconnectCallback(onLayout);
        } else {
          // If we do early preconnects we delay them a bit. This is kind of
          // an unfortunate trade off, but it seems faster, because the DOM
          // operations themselves are not free and might delay
          startupChunk(this.getAmpDoc(), () => {
            if (!this.ownerDocument || !this.ownerDocument.defaultView) {
              return;
            }
            this.impl_.preconnectCallback(onLayout);
          });
        }
      }
  
      /**
       * See `BaseElement.R1()`.
       *
       * @return {boolean}
       * @final
       */
      R1() {
        return this.implClass_ ? this.implClass_.R1() : false;
      }
  
      /**
       * See `BaseElement.deferredMount()`.
       *
       * @return {boolean}
       * @final
       */
      deferredMount() {
        return this.implClass_ ? this.implClass_.deferredMount(this) : false;
      }
  
      /**
       * Whether the custom element declares that it has to be fixed.
       * @return {boolean}
       */
      isAlwaysFixed() {
        return this.impl_ ? this.impl_.isAlwaysFixed() : false;
      }
  
      /**
       * Updates the layout box of the element.
       * Should only be called by Resources.
       * @param {!./layout-rect.LayoutRectDef} layoutBox
       * @param {boolean} sizeChanged
       */
      updateLayoutBox(layoutBox, sizeChanged = false) {
        if (this.isBuilt()) {
          this.onMeasure(sizeChanged);
        }
      }
  
      /**
       * Calls onLayoutMeasure() on the BaseElement implementation.
       * Should only be called by Resources.
       */
      onMeasure() {
        devAssert(this.isBuilt());
        try {
          this.impl_.onLayoutMeasure();
        } catch (e) {
          reportError(e, this);
        }
      }
  
      /**
       * @return {?Element}
       * @private
       */
      getSizer_() {
        if (
          this.sizerElement === undefined &&
          (this.layout_ === Layout.RESPONSIVE ||
            this.layout_ === Layout.INTRINSIC)
        ) {
          // Expect sizer to exist, just not yet discovered.
          this.sizerElement = this.querySelector('i-amphtml-sizer');
        }
        return this.sizerElement || null;
      }
  
      /**
       * @param {Element} sizer
       * @private
       */
      resetSizer_(sizer) {
        if (this.layout_ === Layout.RESPONSIVE) {
          setStyle(sizer, 'paddingTop', '0');
          return;
        }
        if (this.layout_ === Layout.INTRINSIC) {
          const intrinsicSizerImg = sizer.querySelector(
            '.i-amphtml-intrinsic-sizer'
          );
          if (!intrinsicSizerImg) {
            return;
          }
          intrinsicSizerImg.setAttribute('src', '');
          return;
        }
      }
  
      /** @private */
      initMediaAttrs_() {
        const hasMediaAttrs =
          this.hasAttribute('media') ||
          (this.hasAttribute('sizes') &&
            !this.hasAttribute('disable-inline-width')) ||
          this.hasAttribute('heights');
        const hadMediaAttrs = !!this.mediaQueryProps_;
        const win = this.ownerDocument.defaultView;
        if (hasMediaAttrs != hadMediaAttrs && win) {
          if (hasMediaAttrs) {
            this.mediaQueryProps_ = new MediaQueryProps(win, () =>
              this.applyMediaAttrs_()
            );
            this.applyMediaAttrs_();
          } else {
            this.disposeMediaAttrs_();
          }
        }
      }
  
      /** @private */
      disposeMediaAttrs_() {
        if (this.mediaQueryProps_) {
          this.mediaQueryProps_.dispose();
          this.mediaQueryProps_ = null;
        }
      }
  
      /** @private */
      applyMediaAttrs_() {
        const props = this.mediaQueryProps_;
        if (!props) {
          return;
        }
  
        props.start();
  
        // Media query.
        const mediaAttr = this.getAttribute('media') || null;
        const matchesMedia = mediaAttr
          ? props.resolveMatchQuery(mediaAttr)
          : true;
        this.classList.toggle('i-amphtml-hidden-by-media-query', !matchesMedia);
  
        // Sizes.
        const sizesAttr = this.hasAttribute('disable-inline-width')
          ? null
          : this.getAttribute('sizes');
        if (sizesAttr) {
          setStyle(this, 'width', props.resolveListQuery(sizesAttr));
        }
  
        // Heights.
        const heightsAttr =
          this.layout_ === Layout.RESPONSIVE
            ? this.getAttribute('heights')
            : null;
        if (heightsAttr) {
          const sizer = this.getSizer_();
          if (sizer) {
            setStyle(sizer, 'paddingTop', props.resolveListQuery(heightsAttr));
          }
        }
  
        props.complete();
        this.getResource_().requestMeasure();
      }
  
      /**
       * Applies a size change to the element.
       *
       * This method is called by Resources and shouldn't be called by anyone
       * else. This method must always be called in the mutation context.
       *
       * @param {number|undefined} newHeight
       * @param {number|undefined} newWidth
       * @param {!./layout-rect.LayoutMarginsDef=} opt_newMargins
       * @final
       * @package
       */
      applySize(newHeight, newWidth, opt_newMargins) {
        const sizer = this.getSizer_();
        if (sizer) {
          // From the moment height is changed the element becomes fully
          // responsible for managing its height. Aspect ratio is no longer
          // preserved.
          this.sizerElement = null;
          this.resetSizer_(sizer);
          this.mutateOrInvoke_(() => {
            if (sizer) {
              dom.removeElement(sizer);
            }
          });
        }
        if (newHeight !== undefined) {
          setStyle(this, 'height', newHeight, 'px');
        }
        if (newWidth !== undefined) {
          setStyle(this, 'width', newWidth, 'px');
        }
        if (opt_newMargins) {
          if (opt_newMargins.top != null) {
            setStyle(this, 'marginTop', opt_newMargins.top, 'px');
          }
          if (opt_newMargins.right != null) {
            setStyle(this, 'marginRight', opt_newMargins.right, 'px');
          }
          if (opt_newMargins.bottom != null) {
            setStyle(this, 'marginBottom', opt_newMargins.bottom, 'px');
          }
          if (opt_newMargins.left != null) {
            setStyle(this, 'marginLeft', opt_newMargins.left, 'px');
          }
        }
        if (this.isAwaitingSize_()) {
          this.sizeProvided_();
        }
        dom.dispatchCustomEvent(this, AmpEvents.SIZE_CHANGED);
      }
  
      /**
       * Called when the element is first connected to the DOM.
       *
       * This callback is guarded by checks to see if the element is still
       * connected.  Chrome and Safari can trigger connectedCallback even when
       * the node is disconnected. See #12849, https://crbug.com/821195, and
       * https://bugs.webkit.org/show_bug.cgi?id=180940. Thankfully,
       * connectedCallback will later be called when the disconnected root is
       * connected to the document tree.
       *
       * @final
       */
      connectedCallback() {
        if (!isTemplateTagSupported() && this.isInTemplate_ === undefined) {
          this.isInTemplate_ = !!query.closestAncestorElementBySelector(
            this,
            'template'
          );
        }
        if (this.isInTemplate_) {
          return;
        }
  
        if (this.isConnected_ || !dom.isConnectedNode(this)) {
          return;
        }
        this.isConnected_ = true;
  
        if (!this.everAttached) {
          this.classList.add('i-amphtml-element');
          this.classList.add('i-amphtml-notbuilt');
          this.classList.add('amp-notbuilt');
        }
  
        if (!this.ampdoc_) {
          // Ampdoc can now be initialized.
          const win = toWin(this.ownerDocument.defaultView);
          const ampdocService = Services.ampdocServiceFor(win);
          const ampdoc = ampdocService.getAmpDoc(this);
          this.ampdoc_ = ampdoc;
          elementConnectedCallback(ampdoc, this, this.implClass_);
        }
        if (!this.resources_) {
          // Resources can now be initialized since the ampdoc is now available.
          this.resources_ = Services.resourcesForDoc(this.ampdoc_);
        }
        this.getResources().add(this);
  
        if (this.everAttached) {
          const reconstruct = this.reconstructWhenReparented();
          if (reconstruct) {
            this.reset_();
          }
          if (this.isUpgraded()) {
            if (reconstruct && !this.R1()) {
              this.getResources().upgraded(this);
            }
            this.connected_();
            this.dispatchCustomEventForTesting(AmpEvents.ATTACHED);
          }
          if (this.implClass_ && this.R1()) {
            this.upgradeOrSchedule_();
          }
        } else {
          this.everAttached = true;
  
          try {
            this.layout_ = applyStaticLayout(
              this,
              Services.platformFor(toWin(this.ownerDocument.defaultView)).isIe()
            );
            this.initMediaAttrs_();
          } catch (e) {
            reportError(e, this);
          }
          if (this.implClass_) {
            this.upgradeOrSchedule_();
          }
          if (!this.isUpgraded()) {
            this.classList.add('amp-unresolved');
            this.classList.add('i-amphtml-unresolved');
            this.dispatchCustomEventForTesting(AmpEvents.STUBBED);
          }
        }
  
        this.toggleLoading(true);
      }
  
      /**
       * @return {boolean}
       * @private
       */
      isAwaitingSize_() {
        return this.classList.contains('i-amphtml-layout-awaiting-size');
      }
  
      /**
       * @private
       */
      sizeProvided_() {
        this.classList.remove('i-amphtml-layout-awaiting-size');
      }
  
      /**
       * Upgrade or schedule element based on R1.
       * @param {boolean=} opt_disablePreload
       * @private @final
       */
      upgradeOrSchedule_(opt_disablePreload) {
        if (!this.R1()) {
          this.tryUpgrade_();
          return;
        }
  
        if (this.mountPromise_) {
          // Already mounting.
          return;
        }
  
        // Schedule build and mount.
        const scheduler = getSchedulerForDoc(this.getAmpDoc());
        scheduler.schedule(this);
  
        if (this.buildingPromise_) {
          // Already built or building: just needs to be mounted.
          this.setReadyStateInternal(
            this.implClass_ && this.implClass_.usesLoading(this)
              ? ReadyState.LOADING
              : ReadyState.MOUNTING
          );
        } else {
          // Not built yet: execute prebuild steps.
          this.setReadyStateInternal(ReadyState.BUILDING);
  
          // Schedule preconnects.
          if (!opt_disablePreload) {
            const urls = this.implClass_.getPreconnects(this);
            if (urls && urls.length > 0) {
              // If we do early preconnects we delay them a bit. This is kind of
              // an unfortunate trade off, but it seems faster, because the DOM
              // operations themselves are not free and might delay
              const ampdoc = this.getAmpDoc();
              startupChunk(ampdoc, () => {
                const {win} = ampdoc;
                if (!win) {
                  return;
                }
                const preconnect = Services.preconnectFor(win);
                urls.forEach((url) =>
                  preconnect.url(ampdoc, url, /* alsoConnecting */ false)
                );
              });
            }
          }
        }
      }
  
      /**
       * Try to upgrade the element with the provided implementation.
       * @return {!Promise|undefined}
       * @private @final
       */
      tryUpgrade_() {
        if (this.isInTemplate_) {
          return;
        }
        if (this.upgradeState_ != UpgradeState.NOT_UPGRADED) {
          // Already upgraded or in progress or failed.
          return;
        }
  
        const Ctor = devAssert(
          this.implClass_,
          'Implementation must not be a stub'
        );
  
        const impl = new Ctor(this);
  
        // The `upgradeCallback` only allows redirect once for the top-level
        // non-stub class. We may allow nested upgrades later, but they will
        // certainly be bad for performance.
        this.upgradeState_ = UpgradeState.UPGRADE_IN_PROGRESS;
        const startTime = win.Date.now();
        const res = impl.upgradeCallback();
        if (!res) {
          // Nothing returned: the current object is the upgraded version.
          this.completeUpgrade_(impl, startTime);
        } else if (typeof res.then == 'function') {
          // It's a promise: wait until it's done.
          return res
            .then((upgrade) => {
              this.completeUpgrade_(upgrade || impl, startTime);
            })
            .catch((reason) => {
              this.upgradeState_ = UpgradeState.UPGRADE_FAILED;
              rethrowAsync(reason);
            });
        } else {
          // It's an actual instance: upgrade immediately.
          this.completeUpgrade_(
            /** @type {!./base-element.BaseElement} */ (res),
            startTime
          );
        }
      }
  
      /**
       * Called when the element is disconnected from the DOM.
       *
       * @final
       */
      disconnectedCallback() {
        this.disconnect(/* pretendDisconnected */ false);
      }
  
      /** @private */
      connected_() {
        if (this.built_) {
          this.impl_.attachedCallback();
        }
      }
  
      /**
       * Called when an element is disconnected from DOM, or when an ampDoc is
       * being disconnected (the element itself may still be connected to ampDoc).
       *
       * This callback is guarded by checks to see if the element is still
       * connected. See #12849, https://crbug.com/821195, and
       * https://bugs.webkit.org/show_bug.cgi?id=180940.
       * If the element is still connected to the document, you'll need to pass
       * opt_pretendDisconnected.
       *
       * @param {boolean} pretendDisconnected Forces disconnection regardless
       *     of DOM isConnected.
       */
      disconnect(pretendDisconnected) {
        if (this.isInTemplate_ || !this.isConnected_) {
          return;
        }
        if (!pretendDisconnected && dom.isConnectedNode(this)) {
          return;
        }
  
        // This path only comes from Resource#disconnect, which deletes the
        // Resource instance tied to this element. Therefore, it is no longer
        // an AMP Element. But, DOM queries for i-amphtml-element assume that
        // the element is tied to a Resource.
        if (pretendDisconnected) {
          this.classList.remove('i-amphtml-element');
        }
  
        this.isConnected_ = false;
        this.getResources().remove(this);
        if (this.impl_) {
          this.impl_.detachedCallback();
        }
        if (this.R1()) {
          this.unmount();
        }
        this.toggleLoading(false);
        this.disposeMediaAttrs_();
      }
  
      /**
       * Dispatches a custom event only in testing environment.
       *
       * @param {string} name
       * @param {!Object=} opt_data Event data.
       * @final
       */
      dispatchCustomEventForTesting(name, opt_data) {
        if (!getMode().test) {
          return;
        }
        dom.dispatchCustomEvent(this, name, opt_data);
      }
  
      /**
       * Whether the element can pre-render.
       * @return {boolean}
       * @final
       */
      prerenderAllowed() {
        if (this.hasAttribute('noprerender')) {
          return false;
        }
        return this.implClass_ ? this.implClass_.prerenderAllowed(this) : false;
      }
  
      /**
       * Whether the element has render-blocking service.
       * @return {boolean}
       * @final
       */
      isBuildRenderBlocking() {
        return this.impl_ ? this.impl_.isBuildRenderBlocking() : false;
      }
  
      /**
       * Creates a placeholder for the element.
       * @return {?Element}
       * @final
       */
      createPlaceholder() {
        return this.impl_ ? this.impl_.createPlaceholderCallback() : null;
      }
  
      /**
       * Creates a loader logo.
       * @return {{
       *  content: (!Element|undefined),
       *  color: (string|undefined),
       * }}
       * @final
       */
      createLoaderLogo() {
        return this.implClass_
          ? this.implClass_.createLoaderLogoCallback(this)
          : {};
      }
  
      /**
       * Whether the element should ever render when it is not in viewport.
       * @return {boolean|number}
       * @final
       */
      renderOutsideViewport() {
        return this.impl_ ? this.impl_.renderOutsideViewport() : false;
      }
  
      /**
       * Whether the element should render outside of renderOutsideViewport when
       * the scheduler is idle.
       * @return {boolean|number}
       * @final
       */
      idleRenderOutsideViewport() {
        return this.impl_ ? this.impl_.idleRenderOutsideViewport() : false;
      }
  
      /**
       * Returns a previously measured layout box adjusted to the viewport. This
       * mainly affects fixed-position elements that are adjusted to be always
       * relative to the document position in the viewport.
       * @return {!./layout-rect.LayoutRectDef}
       * @final
       */
      getLayoutBox() {
        return this.getResource_().getLayoutBox();
      }
  
      /**
       * Returns a previously measured layout size.
       * @return {!./layout-rect.LayoutSizeDef}
       * @final
       */
      getLayoutSize() {
        return this.getResource_().getLayoutSize();
      }
  
      /**
       * @return {?Element}
       * @final
       */
      getOwner() {
        return this.getResource_().getOwner();
      }
  
      /**
       * Returns a change entry for that should be compatible with
       * IntersectionObserverEntry.
       * @return {?IntersectionObserverEntry} A change entry.
       * @final
       */
      getIntersectionChangeEntry() {
        const box = this.impl_
          ? this.impl_.getIntersectionElementLayoutBox()
          : this.getLayoutBox();
        const owner = this.getOwner();
        const viewport = Services.viewportForDoc(this.getAmpDoc());
        const viewportBox = viewport.getRect();
        // TODO(jridgewell, #4826): We may need to make this recursive.
        const ownerBox = owner && owner.getLayoutBox();
        return getIntersectionChangeEntry(box, ownerBox, viewportBox);
      }
  
      /**
       * Returns the resource of the element.
       * @return {!./service/resource.Resource}
       * @private
       */
      getResource_() {
        return this.getResources().getResourceForElement(this);
      }
  
      /**
       * Returns the resource ID of the element.
       * @return {number}
       */
      getResourceId() {
        return this.getResource_().getId();
      }
  
      /**
       * The runtime calls this method to determine if {@link layoutCallback}
       * should be called again when layout changes.
       * @return {boolean}
       * @package @final
       * TODO(#31915): remove once R1 migration is complete.
       */
      isRelayoutNeeded() {
        return this.impl_ ? this.impl_.isRelayoutNeeded() : false;
      }
  
      /**
       * Returns reference to upgraded implementation.
       * @param {boolean} waitForBuild If true, waits for element to be built before
       *   resolving the returned Promise. Default is true.
       * @return {!Promise<!./base-element.BaseElement>}
       */
      getImpl(waitForBuild = true) {
        const waitFor = waitForBuild ? this.build() : this.createImpl_();
        return waitFor.then(() => this.impl_);
      }
  
      /**
       * @return {!Promise<!./base-element.BaseElement>}
       * @private
       */
      createImpl_() {
        return this.signals_
          .whenSignal(CommonSignals.READY_TO_UPGRADE)
          .then(() => {
            this.tryUpgrade_();
            return this.whenUpgraded();
          });
      }
  
      /**
       * Returns the object which holds the API surface (the thing we add the
       * custom methods/properties onto). In Bento, this is the imperative API
       * object. In AMP, this is the BaseElement instance.
       *
       * @return {!Promise<!Object>}
       */
      getApi() {
        return this.getImpl().then((impl) => impl.getApi());
      }
  
      /**
       * Returns the layout of the element.
       * @return {!Layout}
       */
      getLayout() {
        return this.layout_;
      }
  
      /**
       * Instructs the element to layout its content and load its resources if
       * necessary by calling the {@link BaseElement.layoutCallback} method that
       * should be implemented by BaseElement subclasses. Must return a promise
       * that will yield when the layout and associated loadings are complete.
       *
       * This method is always called for the first layout, but for subsequent
       * layouts the runtime consults {@link isRelayoutNeeded} method.
       *
       * Can only be called on a upgraded and built element.
       *
       * @param {!AbortSignal} signal
       * @return {!Promise}
       * @package @final
       * TODO(#31915): remove once R1 migration is complete.
       */
      layoutCallback(signal) {
        assertNotTemplate(this);
        devAssert(this.isBuilt(), 'Must be built to receive viewport events');
        // A lot of tests call layoutCallback manually, and don't pass a signal.
        if ((!getMode().test || signal) && signal.aborted) {
          return Promise.reject(cancellation());
        }
  
        this.dispatchCustomEventForTesting(AmpEvents.LOAD_START);
        const isLoadEvent = this.layoutCount_ == 0; // First layout is "load".
        this.signals_.reset(CommonSignals.UNLOAD);
        if (isLoadEvent) {
          this.signals_.signal(CommonSignals.LOAD_START);
        }
  
        // Potentially start the loading indicator.
        this.toggleLoading(true);
  
        const promise = tryResolve(() => this.impl_.layoutCallback());
        this.preconnect(/* onLayout */ true);
        this.classList.add('i-amphtml-layout');
  
        return promise.then(
          () => {
            if ((!getMode().test || signal) && signal.aborted) {
              throw cancellation();
            }
            if (isLoadEvent) {
              this.signals_.signal(CommonSignals.LOAD_END);
            }
            this.setReadyStateInternal(ReadyState.COMPLETE);
            this.layoutCount_++;
            this.toggleLoading(false);
            // Check if this is the first success layout that needs
            // to call firstLayoutCompleted.
            if (!this.isFirstLayoutCompleted_) {
              this.impl_.firstLayoutCompleted();
              this.isFirstLayoutCompleted_ = true;
              this.dispatchCustomEventForTesting(AmpEvents.LOAD_END);
            }
          },
          (reason) => {
            if ((!getMode().test || signal) && signal.aborted) {
              throw cancellation();
            }
            // add layoutCount_ by 1 despite load fails or not
            if (isLoadEvent) {
              this.signals_.rejectSignal(
                CommonSignals.LOAD_END,
                /** @type {!Error} */ (reason)
              );
            }
            this.setReadyStateInternal(ReadyState.ERROR, reason);
            this.layoutCount_++;
            this.toggleLoading(false);
            throw reason;
          }
        );
      }
  
      /**
       * Pauses the element.
       *
       * @package @final
       */
      pause() {
        if (!this.isBuilt()) {
          // Not built yet.
          return;
        }
  
        this.impl_.pauseCallback();
  
        // Legacy unlayoutOnPause support.
        if (!this.R1() && this.impl_.unlayoutOnPause()) {
          this.unlayout_();
        }
      }
  
      /**
       * Requests the resource to resume its activity when the document returns
       * from an inactive state. The scope is up to the actual component. Among
       * other things the active playback of video or audio content may be
       * resumed.
       *
       * @package @final
       */
      resume() {
        if (!this.isBuilt()) {
          return;
        }
        this.impl_.resumeCallback();
      }
  
      /**
       * Requests the element to unload any expensive resources when the element
       * goes into non-visible state. The scope is up to the actual component.
       *
       * Calling this method on unbuilt or unupgraded element has no effect.
       *
       * @return {boolean}
       * @package @final
       * TODO(#31915): remove once R1 migration is complete.
       */
      unlayoutCallback() {
        assertNotTemplate(this);
        if (!this.isBuilt()) {
          return false;
        }
        this.signals_.signal(CommonSignals.UNLOAD);
        const isReLayoutNeeded = this.impl_.unlayoutCallback();
        if (isReLayoutNeeded) {
          this.reset_();
        }
        this.dispatchCustomEventForTesting(AmpEvents.UNLOAD);
        return isReLayoutNeeded;
      }
  
      /** @private */
      unlayout_() {
        this.getResource_().unlayout();
        if (this.isConnected_ && this.resources_) {
          this.resources_./*OK*/ schedulePass();
        }
      }
  
      /** @private */
      reset_() {
        this.layoutCount_ = 0;
        this.isFirstLayoutCompleted_ = false;
        this.signals_.reset(CommonSignals.MOUNTED);
        this.signals_.reset(CommonSignals.RENDER_START);
        this.signals_.reset(CommonSignals.LOAD_START);
        this.signals_.reset(CommonSignals.LOAD_END);
        this.signals_.reset(CommonSignals.INI_LOAD);
      }
  
      /**
       * Whether the element needs to be reconstructed after it has been
       * re-parented. Many elements cannot survive fully the reparenting and
       * are better to be reconstructed from scratch.
       *
       * @return {boolean}
       * @package @final
       */
      reconstructWhenReparented() {
        return this.impl_ ? this.impl_.reconstructWhenReparented() : false;
      }
  
      /**
       * Collapses the element, and notifies its owner (if there is one) that the
       * element is no longer present.
       */
      collapse() {
        if (this.impl_) {
          this.impl_./*OK*/ collapse();
        }
      }
  
      /**
       * Called every time an owned AmpElement collapses itself.
       * @param {!AmpElement} element
       */
      collapsedCallback(element) {
        if (this.impl_) {
          this.impl_.collapsedCallback(element);
        }
      }
  
      /**
       * Expands the element, and notifies its owner (if there is one) that the
       * element is now present.
       */
      expand() {
        if (this.impl_) {
          this.impl_./*OK*/ expand();
        }
      }
  
      /**
       * Called when one or more attributes are mutated.
       * Note: Must be called inside a mutate context.
       * Note: Boolean attributes have a value of `true` and `false` when
       *     present and missing, respectively.
       * @param {!JsonObject<string, (null|boolean|string|number|Array|Object)>} mutations
       */
      mutatedAttributesCallback(mutations) {
        if (this.impl_) {
          this.impl_.mutatedAttributesCallback(mutations);
        }
      }
  
      /**
       * Enqueues the action with the element. If element has been upgraded and
       * built, the action is dispatched to the implementation right away.
       * Otherwise the invocation is enqueued until the implementation is ready
       * to receive actions.
       * @param {!./service/action-impl.ActionInvocation} invocation
       * @final
       */
      enqueAction(invocation) {
        assertNotTemplate(this);
        if (!this.isBuilt()) {
          if (this.actionQueue_ === undefined) {
            this.actionQueue_ = [];
          }
          devAssert(this.actionQueue_).push(invocation);
          // Schedule build sooner.
          this.build();
        } else {
          this.executionAction_(invocation, false);
        }
      }
  
      /**
       * Dequeues events from the queue and dispatches them to the implementation
       * with "deferred" flag.
       * @private
       */
      dequeueActions_() {
        if (!this.actionQueue_) {
          return;
        }
  
        const actionQueue = devAssert(this.actionQueue_);
        this.actionQueue_ = null;
  
        // Notice, the actions are currently not de-duped.
        actionQueue.forEach((invocation) => {
          this.executionAction_(invocation, true);
        });
      }
  
      /**
       * Executes the action immediately. All errors are consumed and reported.
       * @param {!./service/action-impl.ActionInvocation} invocation
       * @param {boolean} deferred
       * @final
       * @private
       */
      executionAction_(invocation, deferred) {
        try {
          this.impl_.executeAction(invocation, deferred);
        } catch (e) {
          rethrowAsync(
            'Action execution failed:',
            e,
            invocation.node.tagName,
            invocation.method
          );
        }
      }
  
      /**
       * Get the consent policy to follow.
       * @return {?string}
       */
      getConsentPolicy_() {
        let policyId = this.getAttribute('data-block-on-consent');
        if (policyId === null) {
          if (shouldBlockOnConsentByMeta(this)) {
            policyId = 'default';
            this.setAttribute('data-block-on-consent', policyId);
          } else {
            // data-block-on-consent attribute not set
            return null;
          }
        }
        if (policyId == '' || policyId == 'default') {
          // data-block-on-consent value not set, up to individual element
          // Note: data-block-on-consent and data-block-on-consent='default' is
          // treated exactly the same
          return devAssert(this.impl_).getConsentPolicy();
        }
        return policyId;
      }
  
      /**
       * Get the purpose consents that should be granted.
       * @return {Array<string>|undefined}
       */
      getPurposesConsent_() {
        const purposes =
          this.getAttribute('data-block-on-consent-purposes') || null;
        return purposes?.replace(/\s+/g, '')?.split(',');
      }
  
      /**
       * Returns an optional placeholder element for this custom element.
       * @return {?Element}
       * @package @final
       */
      getPlaceholder() {
        return query.lastChildElement(this, (el) => {
          return (
            el.hasAttribute('placeholder') &&
            // Denylist elements that has a native placeholder property
            // like input and textarea. These are not allowed to be AMP
            // placeholders.
            !isInputPlaceholder(el)
          );
        });
      }
  
      /**
       * Hides or shows the placeholder, if available.
       * @param {boolean} show
       * @package @final
       */
      togglePlaceholder(show) {
        assertNotTemplate(this);
        if (show) {
          const placeholder = this.getPlaceholder();
          if (placeholder) {
            dev().assertElement(placeholder).classList.remove('amp-hidden');
          }
        } else {
          const placeholders = query.childElementsByAttr(this, 'placeholder');
          for (let i = 0; i < placeholders.length; i++) {
            // Don't toggle elements with a native placeholder property
            // e.g. input, textarea
            if (isInputPlaceholder(placeholders[i])) {
              continue;
            }
            placeholders[i].classList.add('amp-hidden');
          }
        }
      }
  
      /**
       * Returns an optional fallback element for this custom element.
       * @return {?Element}
       * @package @final
       */
      getFallback() {
        return query.childElementByAttr(this, 'fallback');
      }
  
      /**
       * Hides or shows the fallback, if available. This function must only
       * be called inside a mutate context.
       * @param {boolean} show
       * @package @final
       */
      toggleFallback(show) {
        assertNotTemplate(this);
        const resourceState = this.getResource_().getState();
        // Do not show fallback before layout
        if (
          !this.R1() &&
          show &&
          (resourceState == ResourceState.NOT_BUILT ||
            resourceState == ResourceState.NOT_LAID_OUT ||
            resourceState == ResourceState.READY_FOR_LAYOUT)
        ) {
          return;
        }
        // This implementation is notably less efficient then placeholder
        // toggling. The reasons for this are: (a) "not supported" is the state of
        // the whole element, (b) some relayout is expected and (c) fallback
        // condition would be rare.
        this.classList.toggle('amp-notsupported', show);
        if (show == true) {
          const fallbackElement = this.getFallback();
          if (fallbackElement) {
            Services.ownersForDoc(this.getAmpDoc()).scheduleLayout(
              this,
              fallbackElement
            );
          }
        }
      }
  
      /**
       * An implementation can call this method to signal to the element that
       * it has started rendering.
       * @package @final
       */
      renderStarted() {
        this.signals_.signal(CommonSignals.RENDER_START);
        this.togglePlaceholder(false);
        this.toggleLoading(false);
      }
  
      /**
       * Whether the loading can be shown for this element.
       * @param {boolean} force
       * @return {boolean}
       * @private
       */
      isLoadingEnabled_(force) {
        // No loading indicator will be shown if either one of these conditions
        // true:
        // 1. The document is A4A.
        // 2. `noloading` attribute is specified;
        // 3. The element has already been laid out, and does not support reshowing the indicator (include having loading
        //    error);
        // 4. The element is too small or has not yet been measured;
        // 5. The element has not been allowlisted;
        // 6. The element is an internal node (e.g. `placeholder` or `fallback`);
        // 7. The element's layout is not nodisplay.
  
        const laidOut =
          this.layoutCount_ > 0 || this.signals_.get(CommonSignals.RENDER_START);
        if (
          this.layout_ == Layout.NODISPLAY ||
          this.hasAttribute('noloading') ||
          (laidOut && !force) ||
          !isLoadingAllowed(this) ||
          query.isInternalOrServiceNode(this)
        ) {
          return false;
        }
  
        return true;
      }
  
      /**
       * Turns the loading indicator on or off.
       * @param {boolean} state
       * @param {boolean=} force
       * @public @final
       */
      toggleLoading(state, force = false) {
        // TODO(dvoytenko, #9177): cleanup `this.ownerDocument.defaultView`
        // once investigation is complete. It appears that we get a lot of
        // errors here once the iframe is destroyed due to timer.
        if (!this.ownerDocument || !this.ownerDocument.defaultView) {
          return;
        }
  
        const loadingIndicator = Services.loadingIndicatorOrNull(
          this.getAmpDoc()
        );
        if (loadingIndicator) {
          state = state && this.isLoadingEnabled_(force);
          if (state) {
            loadingIndicator.track(this);
          } else {
            loadingIndicator.untrack(this);
          }
        }
      }
  
      /**
       * Returns an optional overflow element for this custom element.
       * @return {?Element}
       */
      getOverflowElement() {
        if (this.overflowElement_ === undefined) {
          this.overflowElement_ = query.childElementByAttr(this, 'overflow');
          if (this.overflowElement_) {
            if (!this.overflowElement_.hasAttribute('tabindex')) {
              this.overflowElement_.setAttribute('tabindex', '0');
            }
            if (!this.overflowElement_.hasAttribute('role')) {
              this.overflowElement_.setAttribute('role', 'button');
            }
          }
        }
        return this.overflowElement_;
      }
  
      /**
       * Hides or shows the overflow, if available. This function must only
       * be called inside a mutate context.
       * @param {boolean} overflown
       * @param {number|undefined} requestedHeight
       * @param {number|undefined} requestedWidth
       * @package @final
       */
      overflowCallback(overflown, requestedHeight, requestedWidth) {
        this.getOverflowElement();
        if (!this.overflowElement_) {
          if (overflown && this.warnOnMissingOverflow) {
            user().warn(
              TAG,
              'Cannot resize element and overflow is not available',
              this
            );
          }
        } else {
          this.overflowElement_.classList.toggle('amp-visible', overflown);
  
          if (overflown) {
            this.overflowElement_.onclick = () => {
              const mutator = Services.mutatorForDoc(this.getAmpDoc());
              mutator.forceChangeSize(this, requestedHeight, requestedWidth);
              mutator.mutateElement(this, () => {
                this.overflowCallback(
                  /* overflown */ false,
                  requestedHeight,
                  requestedWidth
                );
              });
            };
          } else {
            this.overflowElement_.onclick = null;
          }
        }
      }
  
      /**
       * Mutates the element using resources if available.
       *
       * @param {function()} mutator
       * @param {?Element=} opt_element
       * @param {boolean=} opt_skipRemeasure
       */
      mutateOrInvoke_(mutator, opt_element, opt_skipRemeasure = false) {
        if (this.ampdoc_) {
          Services.mutatorForDoc(this.getAmpDoc()).mutateElement(
            opt_element || this,
            mutator,
            opt_skipRemeasure
          );
        } else {
          mutator();
        }
      }
    }
    win.__AMP_BASE_CE_CLASS = BaseCustomElement;
    return /** @type {typeof HTMLElement} */ (win.__AMP_BASE_CE_CLASS);
  }
  
  /**
   * @param {!Element} element
   * @return {boolean}
   */
  function isInputPlaceholder(element) {
    return 'placeholder' in element;
  }
  
  /** @param {!Element} element */
  function assertNotTemplate(element) {
    devAssert(!element.isInTemplate_, 'Must never be called in template');
  }
  
  /**
   * Creates a new custom element class prototype.
   *
   * @param {!Window} win The window in which to register the custom element.
   * @param {(typeof ./base-element.BaseElement)=} opt_implementationClass For testing only.
   * @param {function(!./service/ampdoc-impl.AmpDoc, !AmpElement, ?(typeof BaseElement))=} opt_elementConnectedCallback
   * @return {!Object} Prototype of element.
   * @visibleForTesting
   */
  export function createAmpElementForTesting(
    win,
    opt_implementationClass,
    opt_elementConnectedCallback
  ) {
    const Element = createCustomElementClass(
      win,
      opt_elementConnectedCallback || (() => {})
    );
    if (getMode().test && opt_implementationClass) {
      Element.prototype.implementationClassForTesting = opt_implementationClass;
    }
    return Element;
  }
  
  /**
   * @visibleForTesting
   */
  export function resetStubsForTesting() {
    stubbedElements.length = 0;
  }
  
  /**
   * @param {!AmpElement} element
   * @return {?(typeof BaseElement)}
   * @visibleForTesting
   */
  export function getImplClassSyncForTesting(element) {
    return element.implClass_;
  }
  
  /**
   * @param {!AmpElement} element
   * @return {!BaseElement}
   * @visibleForTesting
   */
  export function getImplSyncForTesting(element) {
    return element.impl_;
  }
  
  /**
   * @param {!AmpElement} element
   * @return {?Array<!./service/action-impl.ActionInvocation>|undefined}
   * @visibleForTesting
   */
  export function getActionQueueForTesting(element) {
    return element.actionQueue_;
  }
  