// Copyright 2008 The Closure Library 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.

/**
 * @fileoverview Class that can be used to determine when an iframe is loaded.
 */

goog.provide('goog.net.IframeLoadMonitor');

goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.userAgent');



/**
 * The correct way to determine whether a same-domain iframe has completed
 * loading is different in IE and Firefox.  This class abstracts above these
 * differences, providing a consistent interface for:
 * <ol>
 * <li> Determing if an iframe is currently loaded
 * <li> Listening for an iframe that is not currently loaded, to finish loading
 * </ol>
 *
 * @param {HTMLIFrameElement} iframe An iframe.
 * @param {boolean=} opt_hasContent Whether to wait for the loaded iframe to
 *     have content in its document body.
 * @extends {goog.events.EventTarget}
 * @constructor
 * @final
 */
goog.net.IframeLoadMonitor = function(iframe, opt_hasContent) {
  goog.net.IframeLoadMonitor.base(this, 'constructor');

  /**
   * Iframe whose load state is monitored by this IframeLoadMonitor
   * @type {HTMLIFrameElement}
   * @private
   */
  this.iframe_ = iframe;

  /**
   * Whether to wait for the loaded iframe to have content in its document body.
   * @type {boolean}
   * @private
   */
  this.hasContent_ = !!opt_hasContent;

  /**
   * Whether or not the iframe is loaded.
   * @type {boolean}
   * @private
   */
  this.isLoaded_ = this.isLoadedHelper_();

  if (!this.isLoaded_) {
    // IE 6 (and lower?) does not reliably fire load events, so listen to
    // readystatechange.
    // IE 7 does not reliably fire readystatechange events but listening on load
    // seems to work just fine.
    var isIe6OrLess =
        goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('7');
    var loadEvtType = isIe6OrLess ? goog.events.EventType.READYSTATECHANGE :
                                    goog.events.EventType.LOAD;
    this.onloadListenerKey_ = goog.events.listen(
        this.iframe_, loadEvtType, this.handleLoad_, false, this);

    // Sometimes we still don't get the event callback, so we'll poll just to
    // be safe.
    this.intervalId_ = window.setInterval(
        goog.bind(this.handleLoad_, this),
        goog.net.IframeLoadMonitor.POLL_INTERVAL_MS_);
  }
};
goog.inherits(goog.net.IframeLoadMonitor, goog.events.EventTarget);


/**
 * Event type dispatched by a goog.net.IframeLoadMonitor when it internal iframe
 * finishes loading for the first time after construction of the
 * goog.net.IframeLoadMonitor
 * @type {string}
 */
goog.net.IframeLoadMonitor.LOAD_EVENT = 'ifload';


/**
 * Poll interval for polling iframe load states in milliseconds.
 * @type {number}
 * @private
 */
goog.net.IframeLoadMonitor.POLL_INTERVAL_MS_ = 100;


/**
 * Key for iframe load listener, or null if not currently listening on the
 * iframe for a load event.
 * @type {goog.events.Key}
 * @private
 */
goog.net.IframeLoadMonitor.prototype.onloadListenerKey_ = null;


/**
 * Returns whether or not the iframe is loaded.
 * @return {boolean} whether or not the iframe is loaded.
 */
goog.net.IframeLoadMonitor.prototype.isLoaded = function() {
  return this.isLoaded_;
};


/**
 * Stops the poll timer if this IframeLoadMonitor is currently polling.
 * @private
 */
goog.net.IframeLoadMonitor.prototype.maybeStopTimer_ = function() {
  if (this.intervalId_) {
    window.clearInterval(this.intervalId_);
    this.intervalId_ = null;
  }
};


/**
 * Returns the iframe whose load state this IframeLoader monitors.
 * @return {HTMLIFrameElement} the iframe whose load state this IframeLoader
 *     monitors.
 */
goog.net.IframeLoadMonitor.prototype.getIframe = function() {
  return this.iframe_;
};


/** @override */
goog.net.IframeLoadMonitor.prototype.disposeInternal = function() {
  delete this.iframe_;
  this.maybeStopTimer_();
  goog.events.unlistenByKey(this.onloadListenerKey_);
  goog.net.IframeLoadMonitor.superClass_.disposeInternal.call(this);
};


/**
 * Returns whether or not the iframe is loaded.  Determines this by inspecting
 * browser dependent properties of the iframe.
 * @return {boolean} whether or not the iframe is loaded.
 * @private
 */
goog.net.IframeLoadMonitor.prototype.isLoadedHelper_ = function() {
  var isLoaded = false;

  try {
    if (!this.hasContent_ && goog.userAgent.IE &&
        !goog.userAgent.isVersionOrHigher('11')) {
      // IE versions before IE11 will reliably have readyState set to complete
      // if the iframe is loaded.
      isLoaded = this.iframe_.readyState == 'complete';
    } else {
      // For other browsers, check whether the document body exists to determine
      // whether the iframe has loaded. Older versions of Firefox may fire the
      // LOAD event early for an empty frame and then, a few hundred
      // milliseconds later, replace the contentDocument. If the hasContent
      // check is requested, the iframe is considered loaded only once there is
      // content in the body.
      var body = goog.dom.getFrameContentDocument(this.iframe_).body;
      isLoaded = this.hasContent_ ? !!body && !!body.firstChild : !!body;
    }
  } catch (e) {
    // Ignore these errors. This just means that the iframe is not loaded
    // IE will throw error reading readyState if the iframe is not appended
    // to the dom yet.
    // Firefox will throw error getting the iframe body if the iframe is not
    // fully loaded.
  }
  return isLoaded;
};


/**
 * Handles an event indicating that the loading status of the iframe has
 * changed.  In Firefox this is a goog.events.EventType.LOAD event, in IE
 * this is a goog.events.EventType.READYSTATECHANGED
 * @private
 */
goog.net.IframeLoadMonitor.prototype.handleLoad_ = function() {
  // Only do the handler if the iframe is loaded.
  if (this.isLoadedHelper_()) {
    this.maybeStopTimer_();
    goog.events.unlistenByKey(this.onloadListenerKey_);
    this.onloadListenerKey_ = null;
    this.isLoaded_ = true;
    this.dispatchEvent(goog.net.IframeLoadMonitor.LOAD_EVENT);
  }
};
