/* ************************************************************************

   qooxdoo - the new era of web development

   http://qooxdoo.org

   Copyright:
     2004-2008 1&1 Internet AG, Germany, http://www.1und1.de

   License:
     LGPL: http://www.gnu.org/licenses/lgpl.html
     EPL: http://www.eclipse.org/org/documents/epl-v10.php
     See the LICENSE file in the project's top-level directory for details.

   Authors:
     * Sebastian Werner (wpbasti)

************************************************************************ */

/**
 * EXPERIMENTAL - NOT READY FOR PRODUCTION
 *
 * Loading of local or remote scripts.
 *
 * * Supports cross-domain communication
 * * Automatically "embeds" script so when the loaded event occurs the new features are usable as well
 */
qx.Bootstrap.define("qx.io.ScriptLoader",
{
  construct : function()
  {
    this.__oneventWrapped = qx.Bootstrap.bind(this.__onevent, this);
    this.__elem = document.createElement("script");
  },

  statics :
  {
    /**
     * {Number} Timeout limit in seconds that applies to browsers not supporting
     * the error handler. Default is 15 seconds. 0 means no timeout.
     */
    TIMEOUT: 15
  },

  members :
  {
    /** {Boolean} Whether the request is running */
    __running : null,

    /** {Boolean} Whether the current loader is disposed */
    __disposed : null,

    /** {Function} Callback method to execute */
    __callback : null,

    /** {Object} Context to execute the callback in */
    __context : null,

    /** {Function} This function is a wrapper for the DOM listener */
    __oneventWrapped : null,

    /** {Element} Stores the DOM element of the script tag */
    __elem : null,


    /**
     * Loads the script from the given URL. It is possible to define
     * a callback and a context in which the callback is executed.
     *
     * The callback is executed when the process is done with any
     * of these status messages: success, fail or abort.
     *
     * Note that browsers not supporting the native "error" event detect
     * network errors as soon as the timeout limit is reached.
     *
     * @param url {String} URL of the script
     * @param callback {Function} Callback to execute
     * @param context {Object?window} Context in which the function should be executed
     * @return {void}
     */
    load : function(url, callback, context)
    {
      if (this.__running) {
        throw new Error("Another request is still running!");
      }

      // Since load can be invoked more than one time on the same instance,
      // reset internal status
      this.__running = true;
      this.__disposed = false;

      // Place script element into head
      var head = document.getElementsByTagName("head")[0];

      // Create script element
      var script = this.__elem;

      // Store user data
      this.__callback = callback || null;
      this.__context = context || window;

      // Define mimetype
      script.type = "text/javascript";

      // Attach handlers for all browsers
      script.onerror = script.onload = script.onreadystatechange = this.__oneventWrapped;

      // BUGFIX: Browsers not supporting error handler
      //
      // Note: Because of another browser bug (fires load even though a
      // network error occured), it is virtually useless to work-around
      // for IE < 8. Therefore, only work around for Opera.
      var self = this;
      // no dependency to Environemnt to keep the minimal boot package [BUG #5068]
      if (qx.bom.client.Engine.getName() === "opera" && this._getTimeout() > 0) {
        // No need to clear timeout since on success the callback is called
        // and the loader disposed, meaning the callback is called only once
        setTimeout(function() {
          self.dispose("fail");
        }, this._getTimeout() * 1000);
      }

      // Setup URL
      script.src = url;

      // Finally append child
      // This will execute the script content
      setTimeout(function() {
        // This has to be wrapped in a timeout because under some circumstances
        // the script is evaluated synchronously. (e.g. in IE8 if the script is cached)
        head.appendChild(script);
      }, 0);
    },


    /**
     * Aborts a currently running process.
     *
     * @return {void}
     */
    abort : function()
    {
      if (this.__running) {
        this.dispose("abort");
      }
    },


    /**
     * Internal cleanup method used after every successful
     * or failed loading attempt.
     *
     * @param status {String} Any of success, fail or abort.
     * @return {void}
     */
    dispose : function(status)
    {
      if (this.__disposed) {
        return;
      }
      this.__disposed = true;

      // Get script
      var script = this.__elem;

      // Clear out listeners
      script.onerror = script.onload = script.onreadystatechange = null;

      // Remove script from head
      var scriptParent = script.parentNode;
      if (scriptParent) {
        scriptParent.removeChild(script);
      }

      // Free object
      delete this.__running;

      // Execute user callback
      if (this.__callback)
      {
        // Important to use engine detection directly to keep the minimal
        // package size small [BUG #5068]
        var engineName = qx.bom.client.Engine.getName();
        if (engineName == "mshtml" || engineName == "webkit") {
          // Safari fails with an "maximum recursion depth exceeded" error if
          // many files are loaded

          // IE may call the callback before the content is evaluated if the
          // script is served directly from the browser cache

          var self = this;
          setTimeout(qx.event.GlobalError.observeMethod(function()
          {
            self.__callback.call(self.__context, status);
            delete self.__callback;
          }), 0);
        }
        else
        {
          this.__callback.call(this.__context, status);
          delete this.__callback;
        }
      }
    },


    /**
     * Override to customize timeout limit.
     *
     * Note: Only affects browsers not supporting the error handler (Opera).
     *
     * @return {Number} Timeout limit in seconds
     */
    _getTimeout: function() {
      return qx.io.ScriptLoader.TIMEOUT;
    },


    /**
     * Internal event listener for load and error events.
     *
     * @signature function(e)
     * @param e {Event} Native event object
     */
    __onevent : qx.event.GlobalError.observeMethod(function(e) {
      // Important to use engine detection directly to keep the minimal
      // package size small [BUG #5068]
      var engineName = qx.bom.client.Engine.getName();

      // IE only
      if (engineName == "mshtml") {
        var state = this.__elem.readyState;

        if (state == "loaded") {
          this.dispose("success");
        } else if (state == "complete") {
         this.dispose("success");
        } else {
          return;
        }

      // opera only
      } else if (engineName == "opera") {
        if (qx.Bootstrap.isString(e) || e.type === "error") {
          return this.dispose("fail");
        } else if (e.type === "load") {
          return this.dispose("success");
        } else {
          return;
        }

      /// all other browsers
      } else {
        if (qx.Bootstrap.isString(e) || e.type === "error") {
          this.dispose("fail");
        } else if (e.type === "load") {
          this.dispose("success");
        } else if (e.type === "readystatechange" && (e.target.readyState === "complete" || e.target.readyState === "loaded")) {
          this.dispose("success");
        } else {
          return;
        }
      }
    })
  }
});
