/**
 * Copyright (c) 2007 Peter Michaux. All rights reserved.
 * petermichaux@gmail.com
 * http://forkjavascript.org
 * Code licensed under the MIT License:
 * http://dev.michaux.ca/svn/fork/trunk/public/javascripts/fork/MIT-LICENSE
 */

// API---------------------------------------------
// newXMLHttpRequest (public but you don't need it)
// serializeForm (public but probably you don't need it)
// setButton (use with care)
// request

// Browser Reactions -----------------------------
// NN4.0 Syntax Error reported for ===
// NN4.8 Syntax Error because "try is a reserved identifier"
// NN6.0 isSupported returns false because no XMLHttpRequest support
// NN6.1 isSupported returns false because new XMLHttpRequest object has readyState undefined
// NN6.2 isSupported returns true
// IE4 Syntax Error reported
// IE5.01sp2 isSupported returns false because doesn't have Function.prototype.call()
// IE5.5 isSupported returns true

var FORK = FORK || {};

FORK.Ajax = function(method, url, options) {
  
  this.setOptions(options);
  
  this.method = method.toUpperCase();

  this.request = FORK.Ajax.newXMLHttpRequest();
  if (!this.request) {return true;}

  // With Firefox 1.5 when abort() is called
  // a readystate 4 event is fired
  // which is a disaster. The "aborted" flag is a workaround
  // to avoid having callbacks fired when a request is aborted.
  // The Quirksmode website says that IE has this same bug and given
  // the date of writing it likely refers to IE 6.
  // http://www.quirksmode.org/blog/archives/2005/09/xmlhttp_notes_a_1.html
  this.aborted = false;

  // for handler closures
  var self = this;

  //this.timer;  
  if (this.options.timeout) {
    this.timer = setTimeout(function() {self.onTimeout();}, this.options.timeout);
  }

  this.request.onreadystatechange = function() {self.onReadyStateChange();};

  // begin prep the request body -------------------------------------------
  this.body = this.options.body || {};
  this.setMethod();

  // change the body to a string for sending to server
  this.body = (function(oBody) {
    var aBody = [];
    for (var p in oBody) {
      aBody.push(encodeURIComponent(p) + "=" + encodeURIComponent(oBody[p]));      
    }
    return ((aBody.length > 0) ? aBody.join("&") : null);
  })(this.body);

  var serialization = null;
  if (this.options.form) {
    serialization = FORK.Ajax.serializeForm(this.options.form);
  }

  if (this.body && serialization) {
    this.body = serialization + "&" + this.body;
  } else if (serialization) {
    this.body = serialization;
  }

  if (this.method === 'GET') {
    if (this.body) {
      url = url + ( url.match(/\?/) ? '&' : '?') + this.body;
    }
    this.body = null;
  }
  // end prep the body ------------------------------------------------------

  this.request.open(this.method, url, true);

  if (this.method === "POST") {
    this.request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
  }

  if (this.options.headers) {
    for (p in this.options.headers) {
      this.request.setRequestHeader(p, this.options.headers[p]);
    }
  }
  
  this.request.send(this.body);
};

//FORK.Ajax.request = function(url, method, options) {
//  return new FORK.Ajax(url, method, options);
//};

// factored out for overriding in ajaxRails
FORK.Ajax.prototype.setOptions = function(options) {
  this.options = options || {};
};

// factored out for overriding in ajaxRails
FORK.Ajax.prototype.setMethod = function() {
  if (this.method === 'GET') {
    // Rumor has it that at least one browser caches GET requests
    // made with an XMLHttpRequest object or equivilant ActiveXObject.
    // (TODO find a reference supporting this rumor.)
    // To avoid using any cached response add a
    // unique dummy id to the URL to make it different than all previous
    // URLs and hence insure the request has never been cached.
    this.body._uniqueId = (new Date()).getTime() + "" + FORK.Ajax.transactionId++;
  }
};

FORK.Ajax.transactionId = 0;


FORK.Ajax.newXMLHttpRequest = function() {
  // Possible factories in reverse order of preference 
  // for use with efficient, reverse for-loop below.
  var fs = [
    function() { return new ActiveXObject("Microsoft.XMLHTTP"); },
    function() { return new ActiveXObject("Msxml2.XMLHTTP"); },
    function() { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); },
    function() { return new XMLHttpRequest(); }
  ];

  // Loop through the possible factories to try and find one that 
  // can instantiate an XMLHttpRequest object.
  for (var i=fs.length; i--; ) {
    try {
      // try to instantiate an XMLHttpRequest object
      var r = fs[i]();
      if (r) {
        FORK.Ajax.newXMLHttpRequest = fs[i];
        return r;
      }
    } catch (e) {}
  }

  (FORK.Ajax.newXMLHttpRequest = function() {return null;})();
};

// f the form element
FORK.Ajax.serializeForm = function(f) {
	if (typeof f == 'string') {
    // If f is a string then it can be the form's id or name attribute
		// Could be that only the forms collection is needed because it can find by id also 
		f = document.getElementById(f) || document.forms[f];
	}

	var els = f.elements,
	    cereal = []; // the serialization of the form data into a string

	function add(n, v) { 
		cereal.push(encodeURIComponent(n) + "=" + encodeURIComponent(v));
	}

	for (var i=0, ilen=els.length; i<ilen; i++) {
		var el = els[i];
		if (!el.disabled) {
			switch (el.type) {
				case 'text': case 'password': case 'hidden': case 'textarea':
					add(el.name, el.value);
					break;
				case 'select-one':
					if (el.selectedIndex >= 0) {
						add(el.name, el.options[el.selectedIndex].value);
					}
					break;
				case 'select-multiple':
					for (var j=0, jlen=el.options.length; j<jlen; j++) {
					  var opt = el.options[j];
						if (opt.selected) {
							add(el.name, opt.value);
						}
					}
					break;
				case 'checkbox': case 'radio':
					if (el.checked) {
						add(el.name, el.value);
					}
					break;
			}
		}
	}
	if (this.button) {
	  add(this.button.name, this.button.value);
	  this.button = null;
	}
	return ((cereal.length > 0) ? cereal.join("&") : null);
};


FORK.Ajax.setButton = function(el) {
  this.button = {name:el.name, value:el.value};
};


FORK.Ajax.prototype.doCallback = function(sMethod) {
  if (this.options.scope) {
		// If a scope property is defined, the callback will be fired from
		// the context of the object.
		this.options[sMethod].call(this.options.scope, this.request, this.options.argument);
	} else {
		this.options[sMethod](this.request, this.options.argument);
	}
};

FORK.Ajax.prototype.onReadyStateChange = function() {
  if (!this.aborted && this.request.readyState === 4) {
    if (this.timer) {clearTimeout(this.timer);}
    if (this.request) { // why this conditional?
      this.handleReadyState4();
    }
    // Break circle for IE memory leak
    // Richard Cornford wrote a nice description handling the IE memory leak problem:
    //
    // "There are two good candidates for breaking the circle; the "request"
    // property of the Activation/Variable object refers to the xml http request
    // object, so could have - null - assigned to it after the callback function
    // processes the response, and - onreadystatechange - property of the xml
    // http request object refers to the anonymous function object, and could be
    // re-assigned. Unfortunately assigning a non-function to the -
    // onreadystatechange - property does not work in IE (an allowable quirk of
    // some ActiveX objects), so the value assigned to break the circle would
    // have to be a function, but that function could be a simple (and
    // re-useable) dummy that just did not have a scope chain that held any
    // references to any xml http request obejcts."
    //
    // The original post:
    // http://groups.google.com/group/comp.lang.javascript/msg/556048483801bd96
    //this.request.onreadystatechange = null;
    //this.request.responseText = null;
    this.request = null;
    //this.request.onreadystatechange = FORK.Ajax.emptyFnc;
  }
  
};

//FORK.Ajax.emptyFnc = function(){};

FORK.Ajax.prototype.handleReadyState4 = function() {
  var request = this.request,
      options = this.options;

	var status; // holds the request status
  
  // This try-catch block is taken from YUI connection library
  // which doesn't state under which conditions "request.status"
  // will throw an error. More research about this is required
  // and perhaps the try-catch can be removed with better
  // understanding. However having the try-catch probably doesn't
  // cause any problem.
	try {
		status = request.status;
	} catch(e) {
		// 13030 is the custom code (invented by YUI?) to indicate
		// the condition -- in Mozilla/FF --
		// when the request object's status property is
		// unavailable and a query attempt throws an exception.
		status = 13030;
	}

  // If the request status indicates failure then we make a special
  // object to be passed to the callback handlers as though this object
  // is the request object. The following status value, except the last,
  // are wininet.dll error codes.
  if (status == 12002 || // Server timeout
      status == 12029 || // dropped connections
      status == 12030 || // dropped connections
      status == 12031 || // dropped connections
      status == 12152 || // Connection closed by server.
      status == 13030) { // See above comments for variable status.
    this.request = {status: 0,
                    statusText: "communication failure",
                    argument: options.argument};
  }

  // execute the callbacks -----------------------------------------------
  
  if (options.before) {
		this.doCallback("before");
  }

  this.status = status;
  this.middleCallback();

	if (options.after) {
		this.doCallback("after");
  }
}; // handleReadyState4()

// factored out for overriding in ajaxRails
FORK.Ajax.prototype.middleCallback = function() {
  if (this.options["on"+this.status]) {
		this.doCallback("on"+this.status);
  } else if (this.status >= 200 && this.status < 300 && this.options.onSuccess) {
		this.doCallback("onSuccess");
	} else if ((this.status < 200 || this.status >= 300) && this.options.onFailure) {
    this.doCallback("onFailure");	
	}	else if (this.options.onComplete) {
		this.doCallback("onComplete");
	}	
};

FORK.Ajax.prototype.abort = function() {
  this.aborted = true;
  this.request.abort();
  // Break circle for IE memory leak
  // see notes below
  this.request = null;
  //request.onreadystatechange = FORK.Ajax.emptyFnc;
};


FORK.Ajax.prototype.onTimeout = function() {
  this.aborted = true;
  this.request.abort();
  this.handleTimeout();
  // Break circle for IE memory leak
  // see notes below
  this.request = null;
  //request.onreadystatechange = FORK.Ajax.emptyFnc;
};

FORK.Ajax.prototype.handleTimeout = function() {
  if (this.options.before) {
    this.doCallback("before");
  }
  if (this.options.onTimeout) {
    this.doCallback("onTimeout");
  }
	if (this.options.after) {
	  this.doCallback("after");
	}
};


FORK.Ajax.isSupported = (function(){
  var en = false,
      x;

  // NN6.1 can create an XMLHttpRequest object but it doesn't function
  // and after creation the object's readyState property is 'undefined'
  // when it should be '0'. The XHR object in NN6.1 doesn't work.
  
  try {
    if (typeof (function(){}).call === "function" &&
        (x = FORK.Ajax.newXMLHttpRequest()) && // yes just one equals sign
        x.readyState === 0) {
      en = true;
    }
  } catch(e) {
    en = false;
  }

  // IE 5.01sp2 and errors when x.setRequestHeader runs
  // The error says "object doesn't support this property"
  // I think this is because xhr.open() hasn't been called yet
  //
  // Opera 8 XHR doesn't have setRequestHeader() and although it seems
  // to be able to make a POST request with a body without this function
  // This browser is still going to be put down the degredation path
  // Note Opera 8.0.1 does have setRequestHeader() and can POST
  try {
    if (!x.setRequestHeader) {
      en = false;
    }
  } catch(e) {}


  // Right here NN6.2 will report that "en" is true but 
  // NN6.2 can't make POST requests because can't have arguments to send()
  // so now catch NN6.2 and any other browsers that can't take argument to XHR.send()
  function cannotPost() {
    var xhr = new XMLHttpRequest();
    try {
      xhr.send("asdf");
    } catch (e) {
      // All calls to xhr.send() should error because there wasn't a call to xhr.open()
      // however the normal error is something about "not initialized" as expected
      // since xhr.open() was not called. NN6.2 gives a different error indicating
      // xhr.send() cannot take arguments.
      if (-1 !== e.toString().indexOf("Could not convert JavaScript argument arg 0 [nsIXMLHttpRequest.send]")) {
        return true;
      }
    }
    return false;
  }
  if (this.XMLHttpRequest && cannotPost()) {
    en = false;
  }
  
  return function(){return en;};
})();
