/**
 * Copyright 2018 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 {Services} from '../../../src/services';
import {dev, devAssert, user} from '../../../src/log';
import {dict, hasOwn} from '../../../src/core/types/object';
import {getData} from '../../../src/event-helper';
import {getPageLayoutBoxBlocking} from '../../../src/utils/page-layout-box';
import {getStyle, setStyles} from '../../../src/style';
import {parseUrlDeprecated} from '../../../src/url';
import {throttle} from '../../../src/utils/rate-limit';
import {tryParseJson} from '../../../src/json';

/**
 * Used to manage messages for different Safeframe ad slots.
 *
 * Maps a sentinel value to an instance of the SafeframeHostApi to which that
 * sentinel value belongs.
 * @type {!Object<string, !SafeframeHostApi>}
 */
export const safeframeHosts = {};

/** @private {boolean} */
let safeframeListenerCreated_ = false;

/** @enum {string} */
export const MESSAGE_FIELDS = {
  CHANNEL: 'c',
  SENTINEL: 'e',
  ENDPOINT_IDENTITY: 'i',
  PAYLOAD: 'p',
  SERVICE: 's',
  MESSAGE: 'message',
};

/** @enum {string} */
export const SERVICE = {
  GEOMETRY_UPDATE: 'geometry_update',
  CREATIVE_GEOMETRY_UPDATE: 'creative_geometry_update',
  EXPAND_REQUEST: 'expand_request',
  EXPAND_RESPONSE: 'expand_response',
  REGISTER_DONE: 'register_done',
  COLLAPSE_REQUEST: 'collapse_request',
  COLLAPSE_RESPONSE: 'collapse_response',
  RESIZE_REQUEST: 'resize_request',
  RESIZE_RESPONSE: 'resize_response',
};

/** @private {string} */
const TAG = 'AMP-DOUBLECLICK-SAFEFRAME';

/**
 * Event listener callback for message events. If message is a Safeframe
 * message, handles the message. This listener is registered within
 * SafeframeHostApi.
 * @param {!Event} event
 */
export function safeframeListener(event) {
  const data = tryParseJson(getData(event));
  if (!data) {
    return;
  }
  const payload = tryParseJson(data[MESSAGE_FIELDS.PAYLOAD]) || {};
  /**
   * If the sentinel is provided at the top level, this is a message simply
   * to setup the postMessage channel, so set it up.
   */
  const sentinel = data[MESSAGE_FIELDS.SENTINEL] || payload['sentinel'];
  const safeframeHost = safeframeHosts[sentinel];
  if (!safeframeHost) {
    dev().warn(TAG, `Safeframe Host for sentinel: ${sentinel} not found.`);
    return;
  }
  if (!safeframeHost.equalsSafeframeContentWindow(event.source)) {
    dev().warn(TAG, `Safeframe source did not match event.source.`);
    return;
  }
  if (!safeframeHost.channel) {
    safeframeHost.connectMessagingChannel(data[MESSAGE_FIELDS.CHANNEL]);
  } else if (payload) {
    // Currently we do not expect a payload on initial connection messages.
    safeframeHost.processMessage(
      /** @type {!JsonObject} */ (payload),
      data[MESSAGE_FIELDS.SERVICE]
    );
  }
}

/**
 * Sets up the host API for DoubleClick Safeframe to allow the following
 * Safeframe container APIs to work:
 *   - $sf.ext.expand()
 *   - $sf.ext.collapse()
 *   - $sf.ext.geom() Expand and collapse are both implemented utilizing AMP's
 *     built in element resizing.
 *
 * For geom, the host needs to send geometry updates into the container whenever
 *  a position change happens, at a max frequency of 1 message/second. To
 *  implement this messaging, we are leveraging the existing
 *  IntersectionObserver class that works with AMP elements. However, the
 *  safeframe iframe that we need to monitor is not an AMP element, but rather
 *  contained within an amp-ad. So, we are doing intersection observing on the
 *  amp-ad, and calculating the correct position for the iframe whenever we get
 *  an update.
 *
 * We pass an instance of this class into the IntersectionObserver class, which
 *  then calls the instance of send() below whenever an update occurs.
 */
export class SafeframeHostApi {
  /**
   * @param {!./amp-ad-network-doubleclick-impl.AmpAdNetworkDoubleclickImpl} baseInstance
   * @param {boolean} isFluid
   * @param {{width:number, height:number}} creativeSize
   */
  constructor(baseInstance, isFluid, creativeSize) {
    /** @private {!./amp-ad-network-doubleclick-impl.AmpAdNetworkDoubleclickImpl} */
    this.baseInstance_ = baseInstance;

    /** @private {!Function} */
    this.checkStillCurrent_ = this.baseInstance_.verifyStillCurrent.bind(
      this.baseInstance_
    )();

    /** @private {!Window} */
    this.win_ = this.baseInstance_.win;

    /** @private {string} */
    this.sentinel_ = this.baseInstance_.sentinel;

    /** @private {?Element} */
    this.iframe_ = null;

    /** @type {?string} */
    this.channel = null;

    /** @private {?JsonObject} */
    this.currentGeometry_ = null;

    /** @private {number} */
    this.endpointIdentity_ = Math.random();

    /** @private {number} */
    this.uid_ = Math.random();

    /** @private {boolean} */
    this.isFluid_ = isFluid;

    /** @private {{width:number, height:number}} */
    this.creativeSize_ = creativeSize;

    /** @private {{width:number, height:number}} */
    this.initialCreativeSize_ = /** @type {{width:number, height:number}} */ ({
      ...creativeSize,
    });

    /** @protected {?Promise} */
    this.delay_ = null;

    /** @private {../../../src/service/viewport/viewport-interface.ViewportInterface} */
    this.viewport_ = this.baseInstance_.getViewport();

    /** @private {boolean} */
    this.isCollapsed_ = true;

    /** @private {boolean} */
    this.isRegistered_ = false;

    // TODO: Make this page-level.
    const sfConfig = Object(
      tryParseJson(
        this.baseInstance_.element.getAttribute('data-safeframe-config')
      ) || {}
    );
    /** @private {boolean} */
    this.expandByOverlay_ = hasOwn(sfConfig, 'expandByOverlay')
      ? sfConfig['expandByOverlay']
      : true;

    /** @private {boolean} */
    this.expandByPush_ = hasOwn(sfConfig, 'expandByPush')
      ? sfConfig['expandByPush']
      : true;

    /** @private {?Function} */
    this.unlisten_ = null;

    this.registerSafeframeHost();
  }

  /**
   * Returns true if the given window matches the Safeframe's content window.
   * Comparing to a null window will always return false.
   *
   * @param {Window|null} otherWindow
   * @return {boolean}
   */
  equalsSafeframeContentWindow(otherWindow) {
    return (
      !!otherWindow && otherWindow === this.baseInstance_.iframe.contentWindow
    );
  }

  /**
   * Returns the Safeframe specific name attributes that are needed for the
   * Safeframe creative to properly setup.
   * @return {!JsonObject}
   */
  getSafeframeNameAttr() {
    const attributes = dict({});
    attributes['uid'] = this.uid_;
    attributes['hostPeerName'] = this.win_.location.origin;
    attributes['initialGeometry'] = this.getInitialGeometry();
    attributes['permissions'] = JSON.stringify(
      dict({
        'expandByOverlay': this.expandByOverlay_,
        'expandByPush': this.expandByPush_,
        'readCookie': false,
        'writeCookie': false,
      })
    );
    attributes['metadata'] = JSON.stringify(
      dict({
        'shared': {
          'sf_ver': this.baseInstance_.safeframeVersion,
          'ck_on': 1,
          'flash_ver': '26.0.0',
          // Once GPT Safeframe is updated to look in amp object,
          // remove this canonical_url here.
          'canonical_url': this.maybeGetCanonicalUrl(),
          'amp': {
            'canonical_url': this.maybeGetCanonicalUrl(),
          },
        },
      })
    );
    attributes['reportCreativeGeometry'] = this.isFluid_;
    attributes['isDifferentSourceWindow'] = false;
    attributes['sentinel'] = this.sentinel_;
    return attributes;
  }

  /**
   * Returns the canonical URL of the page, if the publisher allows
   * it to be passed.
   * @return {string|undefined}
   * @visibleForTesting
   */
  maybeGetCanonicalUrl() {
    // Don't allow for referrer policy same-origin,
    // as Safeframe will always be a different origin.
    // Don't allow for no-referrer.
    const ampdoc = this.baseInstance_.getAmpDoc();
    const {canonicalUrl} = Services.documentInfoForDoc(ampdoc);
    const metaReferrer = ampdoc.getMetaByName('referrer');
    if (!metaReferrer) {
      return canonicalUrl;
    }
    switch (metaReferrer) {
      case 'same-origin':
        return;
      case 'no-referrer':
        return;
      case 'origin':
        return parseUrlDeprecated(canonicalUrl).origin;
    }
    return canonicalUrl;
  }

  /**
   * Returns the initialGeometry to assign to the name of the safeframe
   * for rendering. This needs to be done differently than all the other
   * geometry updates, because we don't actually have access to the
   * rendered safeframe yet.
   * @return {string}
   */
  getInitialGeometry() {
    const ampAdBox = getPageLayoutBoxBlocking(this.baseInstance_.element);
    const heightOffset = (ampAdBox.height - this.creativeSize_.height) / 2;
    const widthOffset = (ampAdBox.width - this.creativeSize_.width) / 2;
    const iframeBox = /** @type {!../../../src/layout-rect.LayoutRectDef} */ ({
      top: ampAdBox.top + heightOffset,
      bottom: ampAdBox.bottom - heightOffset,
      left: ampAdBox.left + widthOffset,
      right: ampAdBox.right - widthOffset,
      height: this.initialCreativeSize_.height,
      width: this.initialCreativeSize_.width,
    });
    return this.formatGeom_(iframeBox);
  }

  /**
   * Registers this instance as the host API for the current sentinel.
   * If the global safeframe listener has not yet been created, it creates
   * that as well.
   */
  registerSafeframeHost() {
    devAssert(this.sentinel_);
    safeframeHosts[this.sentinel_] = safeframeHosts[this.sentinel_] || this;
    if (!safeframeListenerCreated_) {
      safeframeListenerCreated_ = true;
      this.win_.addEventListener('message', safeframeListener, false);
    }
  }

  /**
   * Sends initial connection message to the safeframe to finish initialization.
   * Also initializes the sending of geometry update messages to the frame.
   * @param {string} channel
   */
  connectMessagingChannel(channel) {
    // Set the iframe here, because when class is first created the iframe
    // element does not yet exist on this.baseInstance_. The first time
    // we receive a message we know that it now exists.
    devAssert(this.baseInstance_.iframe);
    this.iframe_ = this.baseInstance_.iframe;
    this.channel = channel;
    this.setupGeom_();
    this.sendMessage_(
      {
        'message': 'connect',
        'c': this.channel,
      },
      ''
    );
  }

  /**
   * Creates IntersectionObserver instance for this SafeframeAPI instance.
   * We utilize the existing IntersectionObserver class, by passing in this
   * class for IO to use instead of SubscriptionApi for sending its update
   * messages. The method 'send' below is triggered by IO every time that
   * an update occurs.
   * @private
   */
  setupGeom_() {
    devAssert(this.iframe_.contentWindow, 'Frame contentWindow unavailable.');
    const throttledUpdate = throttle(
      this.win_,
      this.updateGeometry_.bind(this),
      1000
    );
    const scrollUnlistener = this.viewport_.onScroll(throttledUpdate);
    const changedUnlistener = this.viewport_.onChanged(throttledUpdate);
    this.unlisten_ = () => {
      scrollUnlistener();
      changedUnlistener();
    };
    this.updateGeometry_();
  }

  /**
   * Sends a geometry update message into the safeframe.
   * @private
   */
  updateGeometry_() {
    if (!this.iframe_) {
      return;
    }
    this.viewport_
      .getClientRectAsync(this.iframe_)
      .then((iframeBox) => {
        this.checkStillCurrent_();
        const formattedGeom = this.formatGeom_(iframeBox);
        this.sendMessage_(
          {
            newGeometry: formattedGeom,
            uid: this.uid_,
          },
          SERVICE.GEOMETRY_UPDATE
        );
      })
      .catch((err) => dev().error(TAG, err));
  }

  /**
   * Builds geometry update format expected by GPT Safeframe.
   * Also sets this.currentGeometry as side effect.
   * @param {!../../../src/layout-rect.LayoutRectDef} iframeBox The elementRect for the safeframe.
   * @return {string} Safeframe formatted changes.
   * @private
   */
  formatGeom_(iframeBox) {
    const viewportSize = this.viewport_.getSize();
    const scrollLeft = this.viewport_.getScrollLeft();
    const scrollTop = this.viewport_.getScrollTop();
    const currentGeometry = /** @type {JsonObject} */ ({
      'windowCoords_t': 0,
      'windowCoords_r': viewportSize.width,
      'windowCoords_b': viewportSize.height,
      'windowCoords_l': 0,
      'frameCoords_t': iframeBox.top + scrollTop,
      'frameCoords_r': iframeBox.right + scrollLeft,
      'frameCoords_b': iframeBox.bottom + scrollTop,
      'frameCoords_l': iframeBox.left + scrollLeft,
      'posCoords_t': iframeBox.top,
      'posCoords_b': iframeBox.bottom,
      'posCoords_r': iframeBox.right,
      'posCoords_l': iframeBox.left,
      'styleZIndex': getStyle(this.baseInstance_.element, 'zIndex'),
      // AMP's built in resize methodology that we use only allows expansion
      // to the right and bottom, so we enforce that here.
      'allowedExpansion_r': viewportSize.width - iframeBox.width,
      'allowedExpansion_b': viewportSize.height - iframeBox.height,
      'allowedExpansion_t': 0,
      'allowedExpansion_l': 0,
      'yInView': this.getPercInView(
        viewportSize.height,
        iframeBox.top,
        iframeBox.bottom
      ),
      'xInView': this.getPercInView(
        viewportSize.width,
        iframeBox.left,
        iframeBox.right
      ),
    });
    this.currentGeometry_ = currentGeometry;
    return JSON.stringify(currentGeometry);
  }

  /**
   * Helper function to calculate both the xInView and yInView of the
   * geometry update messages. In the case of a 400px wide viewport,
   * with a 100px wide creative that starts at x position 50, if we
   * are calculating xInView, rootBoundEnd is 400, boundingRectStart
   * is 50, and boundingRectEnd is 150.
   * @param {number} rootBoundEnd
   * @param {number} boundingRectStart
   * @param {number} boundingRectEnd
   * @return {number}
   */
  getPercInView(rootBoundEnd, boundingRectStart, boundingRectEnd) {
    const lengthInView =
      boundingRectEnd >= rootBoundEnd
        ? rootBoundEnd - boundingRectStart
        : boundingRectEnd;
    const percInView = lengthInView / (boundingRectEnd - boundingRectStart);
    return Math.max(0, Math.min(1, percInView)) || 0;
  }

  /**
   * Handles serializing and sending messages to the safeframe.
   * @param {!Object} payload
   * @param {string} serviceName
   * @private
   */
  sendMessage_(payload, serviceName) {
    if (!this.iframe_ || !this.iframe_.contentWindow) {
      dev().expectedError(TAG, 'Frame contentWindow unavailable.');
      return;
    }
    const message = dict();
    message[MESSAGE_FIELDS.CHANNEL] = this.channel;
    message[MESSAGE_FIELDS.PAYLOAD] = JSON.stringify(
      /** @type {!JsonObject} */ (payload)
    );
    message[MESSAGE_FIELDS.SERVICE] = serviceName;
    message[MESSAGE_FIELDS.SENTINEL] = this.sentinel_;
    message[MESSAGE_FIELDS.ENDPOINT_IDENTITY] = this.endpointIdentity_;
    this.iframe_.contentWindow./*OK*/ postMessage(JSON.stringify(message), '*');
  }

  /**
   * Routes messages to their appropriate handler.
   * @param {!JsonObject} payload
   * @param {string} service
   */
  processMessage(payload, service) {
    // We are not logging unexpected messages, and some expected
    // messages are being dropped, like init_done, as we don't need them.
    switch (service) {
      case SERVICE.CREATIVE_GEOMETRY_UPDATE:
        this.handleFluidMessage_(payload);
        break;
      case SERVICE.EXPAND_REQUEST:
        this.handleExpandRequest_(payload);
        break;
      case SERVICE.REGISTER_DONE:
        this.isRegistered_ = true;
        break;
      case SERVICE.COLLAPSE_REQUEST:
        this.handleCollapseRequest_();
        break;
      case SERVICE.RESIZE_REQUEST:
        this.handleResizeRequest_(payload);
      default:
        break;
    }
  }

  /**
   * @param {!JsonObject} payload
   * @private
   */
  handleExpandRequest_(payload) {
    if (!this.isRegistered_) {
      return;
    }
    const expandHeight =
      Number(this.creativeSize_.height) +
      payload['expand_b'] +
      payload['expand_t'];
    const expandWidth =
      Number(this.creativeSize_.width) +
      payload['expand_r'] +
      payload['expand_l'];
    // Verify that if expanding by push, that expandByPush is allowed.
    // If expanding by overlay, verify that expandByOverlay is allowed,
    // and that we are only expanding within the bounds of the amp-ad.
    if (
      isNaN(expandHeight) ||
      isNaN(expandWidth) ||
      (payload['push'] && !this.expandByPush_) ||
      (!payload['push'] &&
        !this.expandByOverlay_ &&
        (expandWidth > this.creativeSize_.width ||
          expandHeight > this.creativeSize_.height))
    ) {
      dev().error(TAG, 'Invalid expand values.');
      this.sendResizeResponse(/* SUCCESS? */ false, SERVICE.EXPAND_RESPONSE);
      return;
    }
    // Can't expand to greater than the viewport size
    if (
      expandHeight > this.viewport_.getSize().height ||
      expandWidth > this.viewport_.getSize().width
    ) {
      this.sendResizeResponse(/* SUCCESS? */ false, SERVICE.EXPAND_RESPONSE);
      return;
    }
    this.handleSizeChange(expandHeight, expandWidth, SERVICE.EXPAND_RESPONSE);
  }

  /**
   * @private
   */
  handleCollapseRequest_() {
    // Only collapse if expanded.
    if (this.isCollapsed_ || !this.isRegistered_) {
      this.sendResizeResponse(/* SUCCESS? */ false, SERVICE.COLLAPSE_RESPONSE);
      return;
    }
    this.handleSizeChange(
      this.initialCreativeSize_.height,
      this.initialCreativeSize_.width,
      SERVICE.COLLAPSE_RESPONSE,
      /** isCollapse */ true
    );
  }

  /**
   * @param {number} height
   * @param {number} width
   * @param {string} messageType
   */
  resizeSafeframe(height, width, messageType) {
    this.isCollapsed_ = messageType == SERVICE.COLLAPSE_RESPONSE;
    this.baseInstance_.measureMutateElement(
      /** MEASURER */ () => {
        this.baseInstance_.getResource().measure();
      },
      /** MUTATOR */ () => {
        if (this.iframe_) {
          setStyles(this.iframe_, {
            'height': height + 'px',
            'width': width + 'px',
          });
          this.creativeSize_.height = height;
          this.creativeSize_.width = width;
        }
        this.sendResizeResponse(/** SUCCESS */ true, messageType);
      },
      this.iframe_
    );
  }

  /**
   * Resizes the safeframe, and potentially the containing amp-ad element.
   * Then sends a response message to the Safeframe creative.
   *
   * For expansion:
   *  If the new size is fully contained within the bounds of the amp-ad,
   *  we can resize immediately as there will be no reflow. However, if
   *  the new size is larger than the amp-ad, then first we need to try
   *  to resize the amp-ad, and only resize the safeframe if that succeeds.
   * For collapse:
   *  We always first want to attempt to collapse the amp-ad. Then,
   *  regardless of whether that succeeds, we collapse the safeframe too.
   * @param {number} height In pixels.
   * @param {number} width In pixels.
   * @param {string} messageType
   * @param {boolean=} optIsCollapse Whether this is a collapse attempt.
   * @return {*} TODO(#23582): Specify return type
   */
  handleSizeChange(height, width, messageType, optIsCollapse) {
    return this.viewport_
      .getClientRectAsync(this.baseInstance_.element)
      .then((box) => {
        if (!optIsCollapse && width <= box.width && height <= box.height) {
          this.resizeSafeframe(height, width, messageType);
        } else {
          this.resizeAmpAdAndSafeframe(
            height,
            width,
            messageType,
            optIsCollapse
          );
        }
      });
  }

  /**
   * @param {!JsonObject} payload
   * @private
   */
  handleResizeRequest_(payload) {
    if (!this.isRegistered_) {
      return;
    }
    const resizeHeight =
      Number(this.creativeSize_.height) +
      (payload['resize_b'] + payload['resize_t']);
    const resizeWidth =
      Number(this.creativeSize_.width) +
      (payload['resize_r'] + payload['resize_l']);

    // Make sure we are actually resizing here.
    if (isNaN(resizeWidth) || isNaN(resizeHeight)) {
      dev().error(TAG, 'Invalid resize values.');
      return;
    }

    this.resizeAmpAdAndSafeframe(
      resizeHeight,
      resizeWidth,
      SERVICE.RESIZE_RESPONSE,
      true
    );
  }

  /**
   * @param {boolean} success
   * @param {string} messageType
   */
  sendResizeResponse(success, messageType) {
    if (!this.iframe_) {
      return;
    }
    this.viewport_
      .getClientRectAsync(this.iframe_)
      .then((iframeBox) => {
        this.checkStillCurrent_();
        const formattedGeom = this.formatGeom_(iframeBox);
        this.sendMessage_(
          {
            uid: this.uid_,
            success,
            newGeometry: formattedGeom,
            'expand_t': this.currentGeometry_['allowedExpansion_t'],
            'expand_b': this.currentGeometry_['allowedExpansion_b'],
            'expand_r': this.currentGeometry_['allowedExpansion_r'],
            'expand_l': this.currentGeometry_['allowedExpansion_l'],
            push: true,
          },
          messageType
        );
      })
      .catch((err) => dev().error(TAG, err));
  }

  /**
   * Attempts to resize both the amp-ad and the Safeframe.
   * If the amp-ad can not be resized, then if it was a collapse request,
   * we will still collapse just the safeframe.
   * @param {number} height
   * @param {number} width
   * @param {string} messageType
   * @param {boolean=} opt_isShrinking True if collapsing or resizing smaller.
   */
  resizeAmpAdAndSafeframe(height, width, messageType, opt_isShrinking) {
    // First, attempt to resize the Amp-Ad that is the parent of the
    // safeframe
    this.baseInstance_
      .attemptChangeSize(height, width)
      .then(
        () => {
          this.checkStillCurrent_();
          // If this resize succeeded, we always resize the safeframe.
          // resizeSafeframe also sends the resize response.
          this.resizeSafeframe(height, width, messageType);
        },
        /** REJECT CALLBACK */ () => {
          // If the resize initially failed, it may have been queued
          // as a pendingChangeSize, which will cause the size change
          // to execute upon the next user interaction. We don't want
          // that for safeframe, so we reset it here.
          this.baseInstance_.getResource().resetPendingChangeSize();
          if (opt_isShrinking) {
            // If this is a collapse or resize request, then even if resizing
            // the amp-ad failed, still resize the iframe.
            // resizeSafeframe also sends the resize response.
            // Only register as collapsed if explicitly a collapse request.
            this.resizeSafeframe(height, width, messageType);
          } else {
            // We were attempting to
            // expand past the bounds of the amp-ad, and it failed. Thus,
            // we need to send a failure message, and the safeframe is
            // not resized.
            this.sendResizeResponse(false, messageType);
          }
        }
      )
      .catch((err) => {
        if (err.message == 'CANCELLED') {
          dev().error(TAG, err);
          return;
        }
        dev().error(TAG, `Resizing failed: ${err}`);
        this.sendResizeResponse(false, messageType);
      });
  }

  /**
   * Handles Fluid-related messages dispatched from SafeFrame.
   * @param {!JsonObject} payload
   * @private
   */
  handleFluidMessage_(payload) {
    let newHeight;
    if (!payload || !(newHeight = parseInt(payload['height'], 10))) {
      return;
    }
    this.baseInstance_
      .attemptChangeHeight(newHeight)
      .then(() => {
        this.checkStillCurrent_();
        this.onFluidResize_(newHeight);
      })
      .catch((err) => {
        user().warn(TAG, err);
        const {width, height} = this.baseInstance_.getSlotSize();
        if (width && height) {
          this.onFluidResize_(height);
        }
      });
  }

  /**
   * Fires a delayed impression and notifies the Fluid creative that its
   * container has been resized.
   * @param {number} newHeight The height expanded to.
   * @private
   */
  onFluidResize_(newHeight) {
    const iframe = dev().assertElement(this.baseInstance_.iframe);
    const iframeHeight = parseInt(getStyle(iframe, 'height'), 10) || 0;
    if (iframeHeight != newHeight) {
      setStyles(iframe, {height: `${newHeight}px`});
    }
    this.baseInstance_.fireFluidDelayedImpression();
    // In case we've unloaded in a race condition.
    if (!this.iframe_.contentWindow) {
      return;
    }
    this.iframe_.contentWindow./*OK*/ postMessage(
      JSON.stringify(dict({'message': 'resize-complete', 'c': this.channel})),
      '*'
    );
  }

  /**
   * Unregister this Host API.
   */
  destroy() {
    this.iframe_ = null;
    delete safeframeHosts[this.sentinel_];
    if (this.unlisten_) {
      this.unlisten_();
    }
    if (Object.keys(safeframeHosts).length == 0) {
      removeSafeframeListener();
    }
  }
}

/**
 * Removes the safeframe event listener.
 */
export function removeSafeframeListener() {
  window.removeEventListener('message', safeframeListener, false);
  safeframeListenerCreated_ = false;
}
