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

   qooxdoo - the new era of web development

   http://qooxdoo.org

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

   License:
     MIT: https://opensource.org/licenses/MIT
     See the LICENSE file in the project's top-level directory for details.

   Authors:
     * Sebastian Werner (wpbasti)
     * Andreas Ecker (ecker)
     * Derrell Lipman (derrell)

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

/**
 * Transports requests to a server using the native XmlHttpRequest object.
 *
 * This class should not be used directly by client programmers.
 */

qx.Class.define("qx.io.remote.transport.XmlHttp",
{
  extend : qx.io.remote.transport.Abstract,
  implement: [ qx.core.IDisposable ],


  /*
  *****************************************************************************
     STATICS
  *****************************************************************************
  */

  statics :
  {
    /**
     * Capabilities of this transport type.
     *
     * @internal
     */
    handles :
    {
      synchronous           : true,
      asynchronous          : true,
      crossDomain           : false,
      fileUpload            : false,
      programmaticFormFields : false,
      responseTypes         : [ "text/plain", "text/javascript", "application/json", "application/xml", "text/html" ]
    },


    /**
     * Return a new XMLHttpRequest object suitable for the client browser.
     *
     * @return {Object} native XMLHttpRequest object
     * @signature function()
     *
     * @ignore(XMLHttpRequest)
     */
    createRequestObject : qx.core.Environment.select("engine.name",
    {
      "default" : function() {
        return new XMLHttpRequest;
      },

      // IE7's native XmlHttp does not care about trusted zones. To make this
      // work in the localhost scenario, you can use the following registry setting:
      //
      // [HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\
      // FeatureControl\FEATURE_XMLHTTP_RESPECT_ZONEPOLICY]
      // "Iexplore.exe"=dword:00000001
      //
      // Generally it seems that the ActiveXObject is more stable. jQuery
      // seems to use it always. We prefer the ActiveXObject for the moment, but allow
      // fallback to XMLHTTP if ActiveX is disabled.
      "mshtml" : function()
      {
        if (window.ActiveXObject && qx.xml.Document.XMLHTTP) {
          return new window.ActiveXObject(qx.xml.Document.XMLHTTP);
        }

        if (window.XMLHttpRequest) {
          return new window.XMLHttpRequest;
        }
      }
    }),


    /**
     * Whether the transport type is supported by the client.
     *
     * @return {Boolean} supported or not
     */
    isSupported : function() {
      return !!this.createRequestObject();
    },


    /** The timeout for Xhr requests */
    __timeout: 0,


    /**
     * Sets the timeout for requests
     * @deprecated {6.0} This method is deprecated from the start because synchronous I/O itself is deprecated
     *  in the W3C spec {@link https://xhr.spec.whatwg.org/} and timeouts are indicative of synchronous I/O and/or
     *  other server issues.  However, this API is still supported by many browsers and this API is useful
     *  for code which has not made the transition to asynchronous I/O
     */
    setTimeout: function(timeout) {
      this.__timeout = timeout;
    },


    /**
     * Returns the timeout for requests
     */
    getTimeout: function() {
      return this.__timeout;
    }
  },


  /*
   *****************************************************************************
      PROPERTIES
   *****************************************************************************
   */

   properties :
   {
    /**
     * If true and the responseType property is set to "application/json", getResponseContent() will
     * return a Javascript map containing the JSON contents, i. e. the result qx.lang.Json.parse().
     * If false, the raw string data will be returned and the parsing must be done manually.
     * This is useful for special JSON dialects / extensions which are not supported by
     * qx.lang.Json.
     */
    parseJson :
    {
      check : "Boolean",
      init : true
    }
   },


  /*
  *****************************************************************************
     MEMBERS
  *****************************************************************************
  */

  members :
  {
    /*
    ---------------------------------------------------------------------------
      CORE METHODS
    ---------------------------------------------------------------------------
    */

    __localRequest : false,
    __lastReadyState : 0,
    __request : null,


    /**
     * Returns the native request object
     *
     * @return {Object} native XmlHTTPRequest object
     */
    getRequest : function()
    {
      if (this.__request === null)
      {
        this.__request = qx.io.remote.transport.XmlHttp.createRequestObject();
        this.__request.onreadystatechange = qx.lang.Function.bind(this._onreadystatechange, this);
      }

      return this.__request;
    },




    /*
    ---------------------------------------------------------------------------
      USER METHODS
    ---------------------------------------------------------------------------
    */

    /**
     * Implementation for sending the request
     *
     */
    send : function()
    {
      this.__lastReadyState = 0;

      var vRequest = this.getRequest();
      var vMethod = this.getMethod();
      var vAsynchronous = this.getAsynchronous();
      var vUrl = this.getUrl();

      // --------------------------------------
      //   Local handling
      // --------------------------------------
      var vLocalRequest = (window.location.protocol === "file:" && !(/^http(s){0,1}\:/.test(vUrl)));
      this.__localRequest = vLocalRequest;

      // --------------------------------------
      //   Adding URL parameters
      // --------------------------------------
      var vParameters = this.getParameters(false);
      var vParametersList = [];

      for (var vId in vParameters)
      {
        var value = vParameters[vId];

        if (value instanceof Array)
        {
          for (var i=0; i<value.length; i++) {
            vParametersList.push(encodeURIComponent(vId) + "=" + encodeURIComponent(value[i]));
          }
        }
        else
        {
          vParametersList.push(encodeURIComponent(vId) + "=" + encodeURIComponent(value));
        }
      }

      if (vParametersList.length > 0) {
        vUrl += (vUrl.indexOf("?") >= 0 ? "&" : "?") + vParametersList.join("&");
      }

      // --------------------------------------------------------
      //   Adding data parameters (if no data is already present)
      // --------------------------------------------------------
      if (this.getData() === null)
      {
        var vParameters = this.getParameters(true);
        var vParametersList = [];

        for (var vId in vParameters)
        {
          var value = vParameters[vId];

          if (value instanceof Array)
          {
            for (var i=0; i<value.length; i++)
            {
              vParametersList.push(encodeURIComponent(vId) +
                                   "=" +
                                   encodeURIComponent(value[i]));
            }
          }
          else
          {
            vParametersList.push(encodeURIComponent(vId) +
                                 "=" +
                                 encodeURIComponent(value));
          }
        }

        if (vParametersList.length > 0)
        {
          this.setData(vParametersList.join("&"));
        }
      }

      var encode64 = function(input)
      {
        var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
        var output = "";
        var chr1, chr2, chr3;
        var enc1, enc2, enc3, enc4;
        var i = 0;

        do
        {
          chr1 = input.charCodeAt(i++);
          chr2 = input.charCodeAt(i++);
          chr3 = input.charCodeAt(i++);

          enc1 = chr1 >> 2;
          enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
          enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
          enc4 = chr3 & 63;

          if (isNaN(chr2)) {
            enc3 = enc4 = 64;
          } else if (isNaN(chr3)) {
            enc4 = 64;
          }

          output += keyStr.charAt(enc1) + keyStr.charAt(enc2) + keyStr.charAt(enc3) + keyStr.charAt(enc4);
        }
        while (i < input.length);

        return output;
      };

      // --------------------------------------
      //   Opening connection
      // --------------------------------------
      try
      {
        if (this.getUsername())
        {
          if (this.getUseBasicHttpAuth())
          {
            vRequest.open(vMethod, vUrl, vAsynchronous);
            vRequest.setRequestHeader('Authorization', 'Basic ' + encode64(this.getUsername() + ':' + this.getPassword()));
          }
          else
          {
            vRequest.open(vMethod, vUrl, vAsynchronous, this.getUsername(), this.getPassword());
          }
        }
        else
        {
          vRequest.open(vMethod, vUrl, vAsynchronous);
        }
      }
      catch(ex)
      {
        this.error("Failed with exception: " + ex);
        this.failed();
        return;
      }

      // Apply timeout
      var timeout = qx.io.remote.transport.XmlHttp.getTimeout();
      if (timeout && vAsynchronous) {
        vRequest.timeout = timeout;
      }

      // --------------------------------------
      //   Applying request header
      // --------------------------------------
      // Removed adding a referer header as this is not allowed anymore on most
      // browsers
      // See issue https://github.com/qooxdoo/qooxdoo/issues/9298

      var vRequestHeaders = this.getRequestHeaders();

      for (var vId in vRequestHeaders) {
        vRequest.setRequestHeader(vId, vRequestHeaders[vId]);
      }

      // --------------------------------------
      //   Sending data
      // --------------------------------------
      try {
        if (qx.core.Environment.get("qx.debug"))
        {
          if (qx.core.Environment.get("qx.debug.io.remote.data"))
          {
            this.debug("Request: " + this.getData());
          }
        }

        // IE9 executes the call synchronous when the call is to file protocol
        // See [BUG #4762] for details
        if (
          vLocalRequest && vAsynchronous &&
          qx.core.Environment.get("engine.name") == "mshtml" &&
          (qx.core.Environment.get("engine.version") == 9 &&
           qx.core.Environment.get("browser.documentmode") == 9)
        ) {
          qx.event.Timer.once(function() {
            vRequest.send(this.getData());
          }, this, 0);
        } else {
          vRequest.send(this.getData());
        }
      }
      catch(ex)
      {
        if (vLocalRequest) {
          this.failedLocally();
        }
        else
        {
          this.error("Failed to send data to URL '" + vUrl + "': " + ex, "send");
          this.failed();
        }

        return;
      }

      // --------------------------------------
      //   Readystate for sync requests
      // --------------------------------------
      if (!vAsynchronous) {
        this._onreadystatechange();
      }
    },


    /**
     * Force the transport into the failed state ("failed").
     *
     * This method should be used only if the requests URI was local
     * access. I.e. it started with "file://".
     *
     */
    failedLocally : function()
    {
      if (this.getState() === "failed") {
        return;
      }

      // should only occur on "file://" access
      this.warn("Could not load from file: " + this.getUrl());

      this.failed();
    },




    /*
    ---------------------------------------------------------------------------
      EVENT HANDLER
    ---------------------------------------------------------------------------
    */

    /**
     * Listener method for change of the "readystate".
     * Sets the internal state and informs the transport layer.
     *
     * @signature function(e)
     * @param e {Event} native event
     */
    _onreadystatechange : qx.event.GlobalError.observeMethod(function(e)
    {
      // Ignoring already stopped requests
      switch(this.getState())
      {
        case "completed":
        case "aborted":
        case "failed":
        case "timeout":
          if (qx.core.Environment.get("qx.debug"))
          {
            if (qx.core.Environment.get("qx.debug.io.remote")) {
              this.warn("Ignore Ready State Change");
            }
          }

          return;
      }

      // Checking status code
      var vReadyState = this.getReadyState();

      if (vReadyState == 4)
      {
        // The status code is only meaningful when we reach ready state 4.
        // (Important for Opera since it goes through other states before
        // reaching 4, and the status code is not valid before 4 is reached.)
        if (!qx.io.remote.Exchange.wasSuccessful(this.getStatusCode(), vReadyState, this.__localRequest)) {
          // Fix for bug #2272
          // The IE doesn't set the state to 'sending' even though the send method
          // is called. This only occurs if the server (which is called) goes
          // down or a network failure occurs.
          if (this.getState() === "configured") {
            this.setState("sending");
          }

          this.failed();
          return;
        }
      }

      // Sometimes the xhr call skips the send state
      if (vReadyState == 3 && this.__lastReadyState == 1) {
        this.setState(qx.io.remote.Exchange._nativeMap[++this.__lastReadyState]);
      }

      // Updating internal state
      while (this.__lastReadyState < vReadyState) {
        this.setState(qx.io.remote.Exchange._nativeMap[++this.__lastReadyState]);
      }
    }),




    /*
    ---------------------------------------------------------------------------
      READY STATE
    ---------------------------------------------------------------------------
    */

    /**
     * Get the ready state of this transports request.
     *
     * For qx.io.remote.transport.XmlHttp, ready state is a number between 1 to 4.
     *
     * @return {Integer} ready state number
     */
    getReadyState : function()
    {
      var vReadyState = null;

      try {
        vReadyState = this.getRequest().readyState;
      } catch(ex) {}

      return vReadyState;
    },




    /*
    ---------------------------------------------------------------------------
      REQUEST HEADER SUPPORT
    ---------------------------------------------------------------------------
    */

    /**
     * Set a request header to this transports request.
     *
     * @param vLabel {String} Request header name
     * @param vValue {var} Request header value
     */
    setRequestHeader : function(vLabel, vValue) {
      this.getRequestHeaders()[vLabel] = vValue;
    },




    /*
    ---------------------------------------------------------------------------
      RESPONSE HEADER SUPPORT
    ---------------------------------------------------------------------------
    */

    /**
     * Returns a specific header provided by the server upon sending a request,
     * with header name determined by the argument headerName.
     *
     * Only available at readyState 3 and 4 universally and in readyState 2
     * in Gecko.
     *
     * Please note: Some servers/proxies (such as Selenium RC) will capitalize
     * response header names. This is in accordance with RFC 2616[1], which
     * states that HTTP 1.1 header names are case-insensitive, so your
     * application should be case-agnostic when dealing with response headers.
     *
     * [1]<a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2">RFC 2616: HTTP Message Headers</a>
     *
     * @param vLabel {String} Response header name
     * @return {String|null} Response header value
     */
    getResponseHeader : function(vLabel)
    {
      var vResponseHeader = null;

      try {
        vResponseHeader = this.getRequest().getResponseHeader(vLabel) || null;
      } catch(ex) {}

      return vResponseHeader;
    },


    /**
     * Returns all response headers of the request.
     *
     * @return {var} response headers
     */
    getStringResponseHeaders : function()
    {
      var vSourceHeader = null;

      try
      {
        var vLoadHeader = this.getRequest().getAllResponseHeaders();

        if (vLoadHeader) {
          vSourceHeader = vLoadHeader;
        }
      }
      catch(ex) {}

      return vSourceHeader;
    },


    /**
     * Provides a hash of all response headers.
     *
     * @return {var} hash of all response headers
     */
    getResponseHeaders : function()
    {
      var vSourceHeader = this.getStringResponseHeaders();
      var vHeader = {};

      if (vSourceHeader)
      {
        var vValues = vSourceHeader.split(/[\r\n]+/g);

        for (var i=0, l=vValues.length; i<l; i++)
        {
          var vPair = vValues[i].match(/^([^:]+)\s*:\s*(.+)$/i);

          if (vPair) {
            vHeader[vPair[1]] = vPair[2];
          }
        }
      }

      return vHeader;
    },




    /*
    ---------------------------------------------------------------------------
      STATUS SUPPORT
    ---------------------------------------------------------------------------
    */

    /**
     * Returns the current status code of the request if available or -1 if not.
     *
     * @return {Integer} current status code
     */
    getStatusCode : function()
    {
      var vStatusCode = -1;

      try {
        vStatusCode = this.getRequest().status;

        // [BUG #4476]
        // IE sometimes tells 1223 when it should be 204
        if (vStatusCode === 1223) {
          vStatusCode = 204;
        }

      } catch(ex) {}

      return vStatusCode;
    },


    /**
     * Provides the status text for the current request if available and null
     * otherwise.
     *
     * @return {String} current status code text
     */
    getStatusText : function()
    {
      var vStatusText = "";

      try {
        vStatusText = this.getRequest().statusText;
      } catch(ex) {}

      return vStatusText;
    },




    /*
    ---------------------------------------------------------------------------
      RESPONSE DATA SUPPORT
    ---------------------------------------------------------------------------
    */

    /**
     * Provides the response text from the request when available and null
     * otherwise.  By passing true as the "partial" parameter of this method,
     * incomplete data will be made available to the caller.
     *
     * @return {String} Content of the response as string
     */
    getResponseText : function()
    {
      var vResponseText = null;

      try
      {
        vResponseText = this.getRequest().responseText;
      }
      catch(ex)
      {
        vResponseText = null;
      }

      return vResponseText;
    },


    /**
     * Provides the XML provided by the response if any and null otherwise.  By
     * passing true as the "partial" parameter of this method, incomplete data will
     * be made available to the caller.
     *
     * @return {String} Content of the response as XML
     * @throws {Error} If an error within the response occurs.
     */
    getResponseXml : function()
    {
      var vResponseXML = null;

      var vStatus = this.getStatusCode();
      var vReadyState = this.getReadyState();

      if (qx.io.remote.Exchange.wasSuccessful(vStatus, vReadyState, this.__localRequest))
      {
        try {
          vResponseXML = this.getRequest().responseXML;
        } catch(ex) {}
      }

      // Typical behaviour on file:// on mshtml
      // Could we check this with something like: /^file\:/.test(path); ?
      // No browser check here, because it doesn't seem to break other browsers
      //    * test for this.req.responseXML's objecthood added by *
      //    * FRM, 20050816                                       *
      if (typeof vResponseXML == "object" && vResponseXML != null)
      {
        if (!vResponseXML.documentElement)
        {
          // Clear xml file declaration, this breaks non unicode files (like ones with Umlauts)
          var s = String(this.getRequest().responseText).replace(/<\?xml[^\?]*\?>/, "");
          vResponseXML.loadXML(s);
        }

        // Re-check if fixed...
        if (!vResponseXML.documentElement) {
          throw new Error("Missing Document Element!");
        }

        if (vResponseXML.documentElement.tagName == "parseerror") {
          throw new Error("XML-File is not well-formed!");
        }
      }
      else
      {
        throw new Error("Response was not a valid xml document [" + this.getRequest().responseText + "]");
      }

      return vResponseXML;
    },


    /**
     * Returns the length of the content as fetched thus far
     *
     * @return {Integer} Length of the response text.
     */
    getFetchedLength : function()
    {
      var vText = this.getResponseText();
      return typeof vText == "string" ? vText.length : 0;
    },


    /**
     * Returns the content of the response
     *
     * @return {null | String} Response content if available
     */
    getResponseContent : function()
    {
      var state = this.getState();
      if (state !== "completed" && state != "failed")
      {
        if (qx.core.Environment.get("qx.debug"))
        {
          if (qx.core.Environment.get("qx.debug.io.remote")) {
            this.warn("Transfer not complete or failed, ignoring content!");
          }
        }

        return null;
      }

      if (qx.core.Environment.get("qx.debug"))
      {
        if (qx.core.Environment.get("qx.debug.io.remote")) {
          this.debug("Returning content for responseType: " + this.getResponseType());
        }
      }

      var vText = this.getResponseText();

      if (state == "failed")
      {
          if (qx.core.Environment.get("qx.debug"))
          {
            if (qx.core.Environment.get("qx.debug.io.remote.data"))
            {
              this.debug("Failed: " + vText);
            }
          }

          return vText;
      }

      switch(this.getResponseType())
      {
        case "text/plain":
        case "text/html":
          if (qx.core.Environment.get("qx.debug"))
          {
            if (qx.core.Environment.get("qx.debug.io.remote.data"))
            {
              this.debug("Response: " + vText);
            }
          }

          return vText;

        case "application/json":
          if (qx.core.Environment.get("qx.debug"))
          {
            if (qx.core.Environment.get("qx.debug.io.remote.data"))
            {
              this.debug("Response: " + vText);
            }
          }

          try {
            if (vText && vText.length > 0)
            {
              var ret;
              if (this.getParseJson()){
                ret = qx.lang.Json.parse(vText);
                ret = (ret === 0 ? 0 : (ret || null));
              } else {
                ret = vText;
              }
              return ret;
            }
            else
            {
              return null;
            }
          }
          catch(ex)
          {
            this.error("Could not execute json: [" + vText + "]", ex);
            return "<pre>Could not execute json: \n" + vText + "\n</pre>";
          }

        case "text/javascript":
          if (qx.core.Environment.get("qx.debug"))
          {
            if (qx.core.Environment.get("qx.debug.io.remote.data"))
            {
              this.debug("Response: " + vText);
            }
          }

          try {
            if(vText && vText.length > 0)
            {
              var ret = window.eval(vText);
              return (ret === 0 ? 0 : (ret || null));
            }
            else
            {
              return null;
            }
          } catch(ex) {
            this.error("Could not execute javascript: [" + vText + "]", ex);
            return null;
          }

        case "application/xml":
          vText = this.getResponseXml();

          if (qx.core.Environment.get("qx.debug"))
          {
            if (qx.core.Environment.get("qx.debug.io.remote.data"))
            {
              this.debug("Response: " + vText);
            }
          }

          return (vText === 0 ? 0 : (vText || null));

        default:
          this.warn("No valid responseType specified (" + this.getResponseType() + ")!");
          return null;
      }
    },




    /*
    ---------------------------------------------------------------------------
      APPLY ROUTINES
    ---------------------------------------------------------------------------
    */

    /**
     * Apply method for the "state" property.
     * Fires an event for each state value to inform the listeners.
     *
     * @param value {var} Current value
     * @param old {var} Previous value
     */
    _applyState : function(value, old)
    {
      if (qx.core.Environment.get("qx.debug"))
      {
        if (qx.core.Environment.get("qx.debug.io.remote")) {
          this.debug("State: " + value);
        }
      }

      switch(value)
      {
        case "created":
          this.fireEvent("created");
          break;

        case "configured":
          this.fireEvent("configured");
          break;

        case "sending":
          this.fireEvent("sending");
          break;

        case "receiving":
          this.fireEvent("receiving");
          break;

        case "completed":
          this.fireEvent("completed");
          break;

        case "failed":
          this.fireEvent("failed");
          break;

        case "aborted":
          this.getRequest().abort();
          this.fireEvent("aborted");
          break;

        case "timeout":
          this.getRequest().abort();
          this.fireEvent("timeout");
          break;
      }
    }
  },



  /*
  *****************************************************************************
     DEFER
  *****************************************************************************
  */

  defer : function()
  {
    // basic registration to qx.io.remote.Exchange
    // the real availability check (activeX stuff and so on) follows at the first real request
    qx.io.remote.Exchange.registerType(qx.io.remote.transport.XmlHttp, "qx.io.remote.transport.XmlHttp");
  },




  /*
  *****************************************************************************
     DESTRUCTOR
  *****************************************************************************
  */

  destruct : function()
  {
    var vRequest = this.getRequest();

    if (vRequest)
    {
      // Clean up state change handler
      // Note that for IE the proper way to do this is to set it to a
      // dummy function, not null (Google on "onreadystatechange dummy IE unhook")
      // http://groups.google.com/group/Google-Web-Toolkit-Contributors/browse_thread/thread/7e7ee67c191a6324
      vRequest.onreadystatechange = (function() {});
      // Aborting
      switch(vRequest.readyState)
      {
        case 1:
        case 2:
        case 3:
          vRequest.abort();
      }
    }

    this.__request = null;
  }
});
