// XMLHTTPRequest
// http://www.w3.org/TR/XMLHttpRequest/

// Constructor
function XHR(timeout, retries) {
  if (!XHR.factory)
    throw new Error('XHR: XMLHttpRequest not supported');
  // Actual XMLHttpRequest object
  this.req = XHR.factory();
  // Pseudo readyState property. Used to work around second state of 1 in IE and Firefox
  this._readyState = XHR.READYSTATE_UNINITIALIZED;
  // Work around for IE and Firefox
  this.aborted = false;
  // Set onreadystatechange
  var that = this;
  this.req.onreadystatechange = function(){that._onreadystatechange();};
  // Timeout for reuest in milliseconds. Default value is 30 seconds
  this.timeout = timeout ? timeout : 30000;
  // Ref. to opaque value returned by window.setTimeout for clearing setTimeout
  this.timeoutRef = null;
  // Parameters from open and send call for recreating request when retrying.
  this.retryParams = {};
  // Number of retires. 0 is default
  this.retries = retries ? retries : 0;
}

// Request id. Set and incremented when request is sent
XHR.ridCounter = 0;

// readyState constants
XHR.READYSTATE_UNINITIALIZED = 0;
XHR.READYSTATE_OPEN = 1;
XHR.READYSTATE_SENT = 2;
XHR.READYSTATE_RECIEVING = 3;
XHR.READYSTATE_LOADED = 4;

// Define factory function
// Execute as anomymous function. Only executed once
(function() {
  try {
    // Provoke error if XMLHttpRequest does not exist
    new XMLHttpRequest();
    XHR.factory =
      function() {
        return new XMLHttpRequest();
      };
  }
  catch(e) {  
    try {
      // Provoke error if ActiveXObject("Msxml2.XMLHTTP") does not exist
      new ActiveXObject("Msxml2.XMLHTTP");
      XHR.factory =
        function() {
          return new ActiveXObject("Msxml2.XMLHTTP");
        };
    }
    catch(e) {
      try {
        // Provoke error if ActiveXObject("Microsoft.XMLHTTP") does not exist
        new ActiveXObject("Microsoft.XMLHTTP");
        XHR.factory =
          function() {
            return new ActiveXObject("Microsoft.XMLHTTP");
          };
      }
      catch(e) {
        XHR.factory = null;
      }
    }
  }
})();

// Check whether XMLHttpRequest is supported
XHR.isEnabled =
  function() {
    return XHR.factory ? true : false;
  };

// Generic onreadystatechange for all request
XHR.prototype._onreadystatechange =
  function() {
    // Work around for duplicate state 1 for IE and Firefox
    if (this._readyState == XHR.READYSTATE_OPEN && this.req.readyState == XHR.READYSTATE_OPEN)
      return;
    this._readyState = this.req.readyState;
    // Work around for request.abort triggering change to readystate 4
    // in IE and Firefox
    if (this.req.readyState == XHR.READYSTATE_LOADED && this.aborted)
      return;
    // Switch on request object readystate
    switch (this.req.readyState) {
      // open() not called yet
      case XHR.READYSTATE_UNINITIALIZED:
        this.aborted = false;
        break;
      // open() called, send() not called yet
      case XHR.READYSTATE_OPEN:
        // Set request ID
        this.rid = XHR.ridCounter++;
        // User defined event handler for state
        if (this.onreadystateopen)
          this.onreadystateopen();
        break;
      // send() called, no respons yet
      case XHR.READYSTATE_SENT:
        // Code placed here is *not* executed until repsonse is recieved.
        // This is a bug in IE, Firefox and Safari. It works correctly in Opera 9
        // User defined event handler is moved to XHR.prototype.send function
        break;
      // Recieving respons
      case XHR.READYSTATE_RECIEVING:
        // User defined event handler for state
        if (this.onreadystaterecieving)
          this.onreadystaterecieving();
        break;
      // Response complete
      case XHR.READYSTATE_LOADED:
        // Clear timeout if exist
        if (this.timeoutRef) {
          window.clearTimeout(this.timeoutRef);
          this.timeoutRef = null;
        }
        // Get transmission time
        this.time = (new Date()).getTime() - this.time;
        // User defined event handler for state
        if (this.onreadystateloaded)
          this.onreadystateloaded();
        // Response has been handled. Any cleanup code goes here
        // Break possible circular ref. between XHR and XMLHttpRequest objects
        this.breakRef();
        break
      default:
        break;
    }
  };

// Break cicular reference between XHR and XMLHttpRequest object
XHR.prototype.breakRef =
  function() {
    this.req.onreadystatechange = null;
    this.req = null;
  };

// Reset XHR object prior to resending request
XHR.prototype.reset =
  function() {
    // Break circular reference
    this.breakRef();
    // Reset workaround properties
    this.aborted = false;
    this._readyState = XHR.READYSTATE_UNINITIALIZED;
    // Create new XMLHttpRequest instance
    this.req = XHR.factory();
    // Bind onreadystatechange event handler
    var that = this;
    this.req.onreadystatechange = function(){that._onreadystatechange();};
  };

// Generic event function for timeout
XHR.prototype._ontimeout =
  function() {
    // First action should be to abort. Thus ontimeout is *always* fired after onabort
    this.abort();
    // Clear window.setTimeout opaque value
    this.timeoutRef = null;
    // User event handler for timeout if provided
    if (this.ontimeout)
      this.ontimeout();
    // Retry if there are any retries left
    if (this.retries > 0)
      this.retry();
  };

// Generic event function for retry
XHR.prototype._onretry =
  function() {
    this.retries--;
    // User event handler for retrying if provided
    if (this.onretry)
      this.onretry();
    // Reset XHR obeject (don't know why we have to make a new XMLHttpRequest object)
    this.reset();
    // Re-open command
    this.open(this.retryParams.method, this.retryParams.url, this.retryParams.async, this.retryParams.username, this.retryParams.password);
    // Re-send command
    this.send(this.retryParams.body);
  };

// Retry a request
XHR.prototype.retry =
  function() {
    this._onretry();
  };

// Generic event function for abort
XHR.prototype._onabort =
  function() {
    this.aborted = true;
    // Cancel request. This will trigger a change in readystate to 4 in IE and Firefox
    // Safari does not reset readystate to 0
    this.req.abort();
    // User event handler for abortion if provided
    if (this.onabort)
      this.onabort();
  };

// Abort a request
XHR.prototype.abort =
  function() {
    // Generic event handler for abortion
    this._onabort();
  };

// Open a request
XHR.prototype.open =
  function(method, url, async, username, password) {
    // Save parameters for retry
    this.retryParams.method = method;
    this.retryParams.url = url;
    this.retryParams.async = async;
    this.retryParams.username = username;
    this.retryParams.password = password;
    // Open request
    this.req.open(method, url, async, username, password);
  };

// Send a request
XHR.prototype.send = 
  function(body) {
    if (body === undefined)
      body = null;
    // Save parameters for retry
    this.retryParams.body = body;
    // Set timestamp of request. Have to be done before readyState = 3
    this.time = (new Date()).getTime();
    // setTimeout must be called before readyState = 3
    var that = this;
    // Function arg to setTimeout must be wrapped in anonymous function
    // otherwise 'this' is not resolved correctly
    this.timeoutRef = window.setTimeout(function(){that._ontimeout();}, this.timeout);
    // Send request
    this.req.send(body);
    // User defined event handler for state
    // readyState 2 is handled wrong by IE, Firefox and Safari
    // so functionality must be placed here
    if (this.onreadystatesent)
      this.onreadystatesent();
  };

// Mapping of native getAllResponseHeaders
XHR.prototype.getAllResponseHeaders =
  function() {
    return this.req.getAllResponseHeaders();
  };

// Mapping of native getResponseHeader
XHR.prototype.getResponseHeader =
  function(s) {
    return this.req.getResponseHeader(s);
  };

// Mapping of native setRequestHeader
XHR.prototype.setRequestHeader =
  function(p,v) {
    return this.req.setRequestHeader(p,v);
  };

XHR.prototype.toString =
  function() {
    var s = 'XHR Info\n--------\n';
    s += 'rid: ' + this.rid + '\n'; 
    s += 'timeout: ' + this.timeout + '\n';
    s += 'retries: ' + this.retries + '\n';
    s += 'time: ' + this.time + '\n';
    s += 'aborted: ' + this.aborted + '\n';
    s += 'method: ' + this.retryParams.method + '\n';
    s += 'url: ' + this.retryParams.url + '\n';
    s += 'async: ' + this.retryParams.async + '\n';
    s += 'username: ' + this.retryParams.username + '\n';
    s += 'password: ' + this.retryParams.password + '\n';
    s += 'body: ' + this.retryParams.body + '\n';
    s += 'readyState: ' + this.req.readyState + '\n';
    return s;
  };

// Utility methods
XHR.getFile =
  function(file) {
    
  };