// ===================================================================
// Wrapper for XMLHttpRequest
//
// See design/HTTP.odt.
//
// Concepts:
// - use one status to represent any of
//   * success [0], HTTP 2xx
//   * error detected on client [1..99]
//   * error detected on HTTP transport [100..999]
//   * error specific to server application [1000..1999]
// - when receiving XML, detect "parsererror" and trigger error callback for it
// - use onprogress, onerror, onload (do not use onreadystatechange)
// - set timeout that triggers an error callback if elapsed

var Http;
if (!Http) {
  Http = {};
}
else if (typeof Http != "object") {
  throw new Error("Http already exists and is not an object");
}

if (Http.Client && typeof Http.Client != "function") {
  throw new Error("Http.Client already exists and is not a function: " + Http.Client);
}

// new Http.Client(options)
//
// options:
//
// - onError, onProgress, onLoad (common handling):
//   will be called with 'this' set to the Http.Client instance,
//   this.xhr will contain the XMLHttpRequest object
//   with .status, .statusText, .responseText, .responseXML as handled by XMLHttpRequest.
//
// - onError:
//   this.status will contain a Http.Client.ERR_XX status, or the HTTP status code.
//
// - onProgress:
//   this.progress will contain a number incremented for each call (starting with 1).
//
// - onLoad:
//   this.status will contain the Http.Client.OK status, or the HTTP status code.
//
// - timeout: if specified, the XMLHttpRequest is aborted if it has not completed
//   before the specified number of milliseconds has elapsed.
//   If the timeout elapses, options.onError is called with status set to Http.Client.ERR_TIMEOUT.
//   Default: none.
//
// - userAgent: will be used for HTTP user agent,
//     default: Http.Client.userAgent || windows.navigator.userAgent
//

Http.Client = function(options)
{
  try {
    if (!options) options = {};
    var nullFunc = function() {};

    var defaults = this.constructor.defaults;
    // remember client callbacks this.caller.OnXXX for use in our callbacks this._localOnXXX
    this.callerOnProgress = options.onProgress || defaults.onProgress || nullFunc;
    this.callerOnError    = options.onError    || defaults.onError    || nullFunc;
    this.callerOnLoad     = options.onLoad     || defaults.onLoad     || nullFunc;

    this.timeout          = options.timeout    || defaults.timeout;
    this.userAgent        = options.userAgent  || defaults.userAgent  || window.navigator.userAgent;


    this.xhr = Http.Client._makeXMLHttpRequest();

    // connect this.xhr.onXXX to our callbacks this._localOnXXX
    var that = this;
    // 2009-04-09 HS:
    // see [isStd]
    // IE7 seems to support but not xhr.onload etc.
    // For now, just use onreadystatechange even for non-IE browsers.
    // if (Http.Client._isStd) {
    //   //alert("Http.Client._isStd");
    //   this.xhr.onprogress = function() { that._localOnProgress.call(that); };
    //   this.xhr.onerror    = function() { that._localOnError   .call(that); };
    //   this.xhr.onload     = function() { that._localOnLoad    .call(that); };
    // }
    // else {
      //alert("! Http.Client._isStd");
    this.xhr.onreadystatechange = function() { that._localOnReadyStateChange.call(that); };
    // }

    return this;
  }
  catch (e) {
    alert("Http.Client() failed: " + e);
  }
}

Http.Client.NAME = "Http.Client";

Http.Client.OK           = 0;
Http.Client.ERR_XMLPARSE = 2;
Http.Client.ERR_PREPARE  = 3;
Http.Client.ERR_SEND     = 4;
Http.Client.ERR_TIMEOUT  = 5;

// Default options
// A values the client assigns to one of these variables
// is used as a default value which applies
// if you do not pass a value for the respective option to the constructor.
//
Http.Client.defaults = {
  onProgress: null,
  onError:    null,
  onLoad:     null,
  timeout:    null,
  userAgent:  null
}

// Flanagan, Javascript, 5th ed. 2006, p. 480f.
Http.Client._factories = [
  function() { return new XMLHttpRequest(); },
  function() { return new ActiveXObject("Msxml2.XMLHTTP"); },
  function() { return new ActiveXObject("Microsoft.XMLHTTP"); }
];
Http.Client._factory = null;
Http.Client._makeXMLHttpRequest = function()
{
  if (Http.Client._factory != null) {
    return Http.Client._factory();
  }
  for (var i = 0, len = Http.Client._factories.length; i < len; ++i) {
    try {
      var factory = Http.Client._factories[i];
      var xhr = factory();
      if (xhr) {
        Http.Client._factory = factory;
        // Http.Client._isStd = (i == 0); // see [isStd]
        return xhr;
      }
    }
    catch(e) {}
  }
  Http.Client._factory = function() { throw new Error("XMLHttpRequest not supported"); }
  Http.Client._factory(); // throw error
}

Http.Client.prototype._notifyOnErrorWithStatus = function(status)
{
  this.status = status;
  this.callerOnError.call(this);
}

Http.Client.prototype._notifyOnLoadWithStatus = function(status)
{
  this.status = status;
  this.callerOnLoad.call(this);
}

// extractServerStatus(xhr):
// returns a numeric code,
// either extracted from xhr.statusText, or just xhr.status.
// Override for different convention of storing server status code in xhr.statusText.

Http.Client.prototype.extractServerStatus = function(xhr)
{
  // check for server application error like "{E404:1093}"
  var appErr = xhr.statusText.match(/\{E(\d+)?:(\d+)\}/);
  return appErr ? Number(appErr[2]) : xhr.status;
}

Http.Client.prototype._localOnProgress = function()
{
  try {
    ++this.progress;
    this.callerOnProgress.call(this);
  }
  catch (e) {
    alert("Http.Client._localOnProgress failed: " + e);
  }
}

Http.Client.prototype._localOnError = function()
{
  try {
    // do not abort - it resets response data (?)
    this.Loading = false;
    if (this.xhr.timer) {
      clearTimeout(this.xhr.timer);
    }
    //alert("Http.Client._localOnError");
    this._notifyOnErrorWithStatus(this.xhr.status);
  }
  catch (e) {
    alert("Http.Client._localOnError failed: " + e);
  }
}

Http.Client.prototype._localOnReadyStateChange = function()
{
  try {
    if (this.xhr.readyState == 3) {
      this._localOnProgress();
    }
    else if (this.xhr.readyState == 4) {
      if (this.xhr.status == 200 || this.xhr.status == 0) { // test on 0 for synchronous call
        this._localOnLoad();
      }
      else if (this.xhr.status >= 400) {
        this._localOnError();
      }
    }
  }
  catch (e) {
    alert("Http.Client._localOnReadyStateChange failed: " + e);
  }
}


Http.Client.prototype._localOnLoad = function()
{
  //alert("Http.Client._localOnLoad");
  try {
    // do not abort - it resets response data (?)
    this.Loading = false;
    if (this.xhr.timer) {
      clearTimeout(this.xhr.timer);
    }

    if (this.xhr.status >= 300) {
      // http://www.ietf.org/rfc/rfc2616
      // - 3xx: Redirection - Further action must be taken in order to complete the request
      //   Flanagan p. 939: send() for synchronous call,
      //   background thread follows the redirect automatically!
      // - 4xx: Client Error - The request contains bad syntax or cannot be fulfilled
      // - 5xx: Server Error - The server failed to fulfill an apparently valid request
      this._notifyOnErrorWithStatus(this.extractServerStatus(this.xhr));
      return;
    }

    if (this.xhr.status != 204 && this.xhr.status != 205
        && this.xhr.responseXML
        // responseXML for status 200 non null, but responseXML.documentElement null in IE6
        && this.xhr.responseXML.documentElement
        // documentElement.localName not available in IE6 (DOM2, Flanagan p. 862)!
        && this.xhr.responseXML.documentElement.tagName.toLowerCase() == "parsererror") {
      this._notifyOnErrorWithStatus(Http.Client.ERR_XMLPARSE);
    }
    else {
      this._notifyOnLoadWithStatus(Http.Client.OK);
    }
  }
  catch (e) {
    alert("Http.Client._localOnLoad failed: " + e.name + ": "+ e.message);
  }
}

Http.Client.prototype._send = function(method, url, data, options)
{
  try {
    if (!options) options = {};
    if (this.Loading) {
      this.abort(); // resets this.Loading() + timeout
    }
    this.xhr.open(method, url);

    // userAgent
    this.xhr.setRequestHeader("User-Agent", this.userAgent);

    // progress
    if (this.onProgress) {
      this.progress = 0; // set start value
    }

//TODO low 2009-03-14 HS - allow passing in request headers

    if (method == "POST") {
      // Flanagan p. 488
      this.xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

      // 2008-10-29 HS: M. Nagy reports Http.Client error 411 when using ParolEdit behind proxy
      // (Firefox does not ask for user/pass)
      // => adding Content-Length does not make a difference in my tests, maybe helps the proxy.
      //
      // Cf. Http.Client spec
      // - ch. 4.4:
      //   The transfer-length of a message is the length of the message-body
      //   as it appears in the message; that is, after any transfer-codings have been applied.
      //   ... If a Content-Length header field (section 14.13) is present,
      //   its decimal value in OCTETs represents both the entity-length and the transfer-length.
      //   The Content-Length header field MUST NOT be sent if these two lengths are different
      //   (i.e., if a Transfer-Encoding header field is present).
      // - ch. 14.13
      //   The Content-Length entity-header field indicates the size of the entity-body,
      //   in decimal number of OCTETs
      // HTTP spec talks about Content-Length vs. Transfer-Length,
      // http://www.w3.org/Protocols/rfc2616/rfc2616.html
      this.xhr.setRequestHeader("Content-Length", data ? data.length : 0);
    }
    //this.xhr.overrideMimeType("application/xml");

    // timeout
    // Flanagan, Javascript, 5th ed. 2006, p. 492
    if (options.timeout) {
      var that = this;
      this.xhr.timer = setTimeout(
        function() {
          that.xhr.timer = null; // prevent from calling clearTimeout()
          that.abort(); // abort before notify
          that._notifyOnErrorWithStatus(Http.Client.ERR_TIMEOUT);
        },
        this.xhr.timeout);
    }
  }
  catch (e) {
    this._notifyOnErrorWithStatus(Http.Client.ERR_PREPARE);
    return null; // cf. sage: run into...
  }

  try {
    this.Loading = true;
    this.xhr.send(data);
  }
  catch (e) {
    this._notifyOnErrorWithStatus(Http.Client.ERR_SEND);
  }
  return null;
}

// === Interface methods ===

// abort()
// stop any pending get() or send()

Http.Client.prototype.abort = function()
{
  if (this.xhr.timer) {
    clearTimeout(this.xhr.timer);
  }
  this.xhr.abort();
  this.Loading = false;
}

// loadText(url)
// - synchronous loading (preferred for file:///)
// - returns result text or null

Http.Client.prototype.loadText = function(url)
{
  try {
    if (this.Loading) {
      this.abort();
    }
    if (url.indexOf("://") == -1) {
      url = "file://" + url;
    }

    // http://developer.mozilla.org/en/docs/XMLHttpRequest
    this.xhr.open("GET", url, false);
    this.xhr.send(null);
    // for "file:///", this.xhr.status is 0 on success!!!
    return this.xhr ? this.xhr.responseText : null;
  }
  catch (e) {
    alert("Http.Client.loadText: " + e);
  }
}

// loadXML(url)
// - synchronous loading (preferred for file:///)
// - returns result dom or null

Http.Client.prototype.loadXML = function(url)
{
  try {
    if (this.Loading) {
      this.abort();
    }
    if (url.indexOf("://") == -1) {
      url = "file://" + url;
    }

    // http://developer.mozilla.org/en/docs/XMLHttpRequest
    this.xhr.open("GET", url, false);
    this.xhr.overrideMimeType('text/xml');
    this.xhr.send(null);
    // for "file:///", this.xhr.status is 0 on success!!!
    return this.xhr ? this.xhr.responseXML : null;
  }
  catch (e) {
    alert("Http.Client.loadXML: " + e);
  }
}

// get(url, options)
// - retrieves url via HTTP
// - returns null.
//
// Options:
//
// - none so far

Http.Client.prototype.get = function(url, options)
{
  if (url.indexOf("://") == -1) {
    url = "file://" + url;
  }
  return this._send("GET", url, null, options);
}

// send(url, data, options)
// - sends data to url via HTTP,
// - returns null.
//
// Options:
//
// - none so far

Http.Client.prototype.post = function(url, data, options)
{
  return this._send("POST", url, data, options);
}

// "delete" is a reserved JavaScript keyword
Http.Client.prototype.Delete = function(url, options)
{
  return this._send("DELETE", url, null, options);
}

