/**
 * The AjaxRequest class is a wrapper for the XMLHttpRequest objects which 
 * are available in most modern browsers. It simplifies the interfaces for
 * making Ajax requests, adds commonly-used convenience methods, and makes 
 * the process of handling state changes more intuitive.
 * An object may be instantiated and used, or the Class methods may be used 
 * which internally create an AjaxRequest object.
 */
function AjaxRequest() {
   var req = new Object();
   
   // -------------------
   // Instance properties
   // -------------------

   /**
    * Timeout period (in ms) until an async request will be aborted, and
    * the onTimeout function will be called
    */
   req.timeout = null;
   
   /**
    * Since some browsers cache GET requests via XMLHttpRequest, an
    * additional parameter called AjaxRequestUniqueId will be added to
    * the request URI with a unique numeric value appended so that the requested
    * URL will not be cached.
    */
   req.generateUniqueUrl = true;
   
   /**
    * The url that the request will be made to, which defaults to the current 
    * url of the window
    */
   req.url = window.location.href;
   
   /**
    * The method of the request, either GET (default), POST, or HEAD
    */
   req.method = "GET";
   
   /**
    * Whether or not the request will be asynchronous. In general, synchronous 
    * requests should not be used so this should rarely be changed from true
    */
   req.async = true;
   
   /**
    * The username used to access the URL
    */
   req.username = null;
   
   /**
    * The password used to access the URL
    */
   req.password = null;
   
   /**
    * The parameters is an object holding name/value pairs which will be 
    * added to the url for a GET request or the request content for a POST request
    */
   req.parameters = new Object();
   
   /**
    * The sequential index number of this request, updated internally
    */
   req.requestIndex = AjaxRequest.numAjaxRequests++;
   
   /**
    * Indicates whether a response has been received yet from the server
    */
   req.responseReceived = false;
   
   /**
    * The name of the group that this request belongs to, for activity 
    * monitoring purposes
    */
   req.groupName = null;
   
   /**
    * The query string to be added to the end of a GET request, in proper 
    * URIEncoded format
    */
   req.queryString = "";
   
   /**
    * After a response has been received, this will hold the text contents of 
    * the response - even in case of error
    */
   req.responseText = null;
   
   /**
    * After a response has been received, this will hold the XML content
    */
   req.responseXML = null;
   
   /**
    * After a response has been received, this will hold the status code of 
    * the response as returned by the server.
    */
   req.status = null;
   
   /**
    * After a response has been received, this will hold the text description 
    * of the response code
    */
   req.statusText = null;

   /**
    * An internal flag to indicate whether the request has been aborted
    */
   req.aborted = false;
   
   /**
    * The XMLHttpRequest object used internally
    */
   req.xmlHttpRequest = null;

   // --------------
   // Event handlers
   // --------------
   
   /**
    * If a timeout period is set, and it is reached before a response is 
    * received, a function reference assigned to onTimeout will be called
    */
   req.onTimeout = null; 
   
   /**
    * A function reference assigned will be called when readyState=1
    */
   req.onLoading = null;

   /**
    * A function reference assigned will be called when readyState=2
    */
   req.onLoaded = null;

   /**
    * A function reference assigned will be called when readyState=3
    */
   req.onInteractive = null;

   /**
    * A function reference assigned will be called when readyState=4
    */
   req.onComplete = null;

   /**
    * A function reference assigned will be called after onComplete, if 
    * the statusCode=200
    */
   req.onSuccess = null;

   /**
    * A function reference assigned will be called after onComplete, if 
    * the statusCode != 200
    */
   req.onError = null;
   
   /**
    * If this request has a group name, this function reference will be called 
    * and passed the group name if this is the first request in the group to 
    * become active
    */
   req.onGroupBegin = null;

   /**
    * If this request has a group name, and this request is the last request 
    * in the group to complete, this function reference will be called
    */
   req.onGroupEnd = null;

   // Get the XMLHttpRequest object itself
   req.xmlHttpRequest = AjaxRequest.getXmlHttpRequest();
   if (req.xmlHttpRequest==null) { return null; }
   
   // -------------------------------------------------------
   // Attach the event handlers for the XMLHttpRequest object
   // -------------------------------------------------------
   req.xmlHttpRequest.onreadystatechange = 
   function() {
      if (req==null || req.xmlHttpRequest==null) { 
         return; 
      }

      if (req.xmlHttpRequest.readyState==1) { 
         req.onLoadingInternal(req); 
      }

      if (req.xmlHttpRequest.readyState==2) { 
         req.onLoadedInternal(req); 
      }

      if (req.xmlHttpRequest.readyState==3) { 
         req.onInteractiveInternal(req); 
      }

      if (req.xmlHttpRequest.readyState==4) { 
         req.onCompleteInternal(req); 
      }
   };
   
   // ---------------------------------------------------------------------------
   // Internal event handlers that fire, and in turn fire the user event handlers
   // ---------------------------------------------------------------------------
   // Flags to keep track if each event has been handled, in case of 
   // multiple calls (some browsers may call the onreadystatechange 
   // multiple times for the same state)
   req.onLoadingInternalHandled = false;
   req.onLoadedInternalHandled = false;
   req.onInteractiveInternalHandled = false;
   req.onCompleteInternalHandled = false;

   req.onLoadingInternal = 
      function() {
         if (req.onLoadingInternalHandled) { 
            return; 
         }

         AjaxRequest.numActiveAjaxRequests++;
         if (AjaxRequest.numActiveAjaxRequests == 1 && typeof(window['AjaxRequestBegin']) == "function") {
            AjaxRequestBegin();
         }

         if (req.groupName != null) {
            if (typeof(AjaxRequest.numActiveAjaxGroupRequests[req.groupName]) == "undefined") {
               AjaxRequest.numActiveAjaxGroupRequests[req.groupName] = 0;
            }

            AjaxRequest.numActiveAjaxGroupRequests[req.groupName]++;
            if (AjaxRequest.numActiveAjaxGroupRequests[req.groupName] == 1 && typeof(req.onGroupBegin) == "function") {
               req.onGroupBegin(req.groupName);
            }
         }

         if (typeof(req.onLoading) == "function") {
            req.onLoading(req);
         }

         req.onLoadingInternalHandled = true;
      };

   req.onLoadedInternal = 
      function() {
         if (req.onLoadedInternalHandled) { 
            return; 
         }

         if (typeof(req.onLoaded) == "function") {
            req.onLoaded(req);
         }

         req.onLoadedInternalHandled = true;
      };

   req.onInteractiveInternal = 
      function() {
         if (req.onInteractiveInternalHandled) { 
            return; 
         }

         if (typeof(req.onInteractive) == "function") {
            req.onInteractive(req);
         }

         req.onInteractiveInternalHandled = true;
      };

   req.onCompleteInternal = 
      function() {
         if (req.onCompleteInternalHandled || req.aborted) { 
            return; 
         }

         req.onCompleteInternalHandled = true;
         AjaxRequest.numActiveAjaxRequests--;

         if (AjaxRequest.numActiveAjaxRequests == 0 && typeof(window['AjaxRequestEnd']) == "function") {
            AjaxRequestEnd(req.groupName);
         }

         if (req.groupName!=null) {
            AjaxRequest.numActiveAjaxGroupRequests[req.groupName]--;

            if (AjaxRequest.numActiveAjaxGroupRequests[req.groupName] == 0 && typeof(req.onGroupEnd) == "function") {
               req.onGroupEnd(req.groupName);
            }
         }

         req.responseReceived = true;
         req.status = req.xmlHttpRequest.status;
         req.statusText = req.xmlHttpRequest.statusText;
         req.responseText = req.xmlHttpRequest.responseText;
         req.responseXML = req.xmlHttpRequest.responseXML;

         if (typeof(req.onComplete) == "function") {
            req.onComplete(req);
         }

         if (req.xmlHttpRequest.status == 200 && typeof(req.onSuccess) == "function") {
            req.onSuccess(req);
         }

         else if (typeof(req.onError) == "function") {
            req.onError(req);
         }

         // Clean up so IE doesn't leak memory
         delete req.xmlHttpRequest['onreadystatechange'];
         req.xmlHttpRequest = null;
      };

   req.onTimeoutInternal = 
      function() {
         if (req != null && req.xmlHttpRequest != null && !req.onCompleteInternalHandled) {
            req.aborted = true;
            req.xmlHttpRequest.abort();
            AjaxRequest.numActiveAjaxRequests--;

            if (AjaxRequest.numActiveAjaxRequests == 0 && typeof(window['AjaxRequestEnd']) == "function") {
               AjaxRequestEnd(req.groupName);
            }

            if (req.groupName != null) {
               AjaxRequest.numActiveAjaxGroupRequests[req.groupName]--;
               if (AjaxRequest.numActiveAjaxGroupRequests[req.groupName] == 0 && typeof(req.onGroupEnd) == "function") {
                  req.onGroupEnd(req.groupName);
               }
            }

            if (typeof(req.onTimeout)=="function") {
               req.onTimeout(req);
            }

            // Opera won't fire onreadystatechange after abort, but other browsers do. 
            // So we can't rely on the onreadystate function getting called. Clean up here!
            delete req.xmlHttpRequest['onreadystatechange'];
            req.xmlHttpRequest = null;
         }
      };

   // ----------------
   // Instance methods
   // ----------------
   /**
    * The process method is called to actually make the request. It builds the
    * querystring for GET requests (the content for POST requests), sets the
    * appropriate headers if necessary, and calls the 
    * XMLHttpRequest.send() method
   */
   req.process = 
      function() {
         if (req.xmlHttpRequest != null) {
            // Some logic to get the real request URL
            if (req.generateUniqueUrl && req.method == "GET") {
               req.parameters["AjaxRequestUniqueId"] = new Date().getTime() + "" + req.requestIndex;
            }

            var content = null; // For POST requests, to hold query string
            for (var i in req.parameters) {
               if (req.queryString.length > 0) { 
                  req.queryString += "&"; 
               }
               req.queryString += encodeURIComponent(i) + "=" + encodeURIComponent(req.parameters[i]);
            }

            if (req.method == "GET") {
               if (req.queryString.length > 0) {
                  req.url += ((req.url.indexOf("?") > -1) ? "&" : "?") + req.queryString;
               }
            }

            req.xmlHttpRequest.open(req.method, req.url, req.async, req.username, req.password);
            if (req.method == "POST") {
               if (typeof(req.xmlHttpRequest.setRequestHeader)!="undefined") {
                  req.xmlHttpRequest.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
               }

               content = req.queryString;
            }

            if (req.timeout > 0) {
               setTimeout(req.onTimeoutInternal,req.timeout);
            }

            req.xmlHttpRequest.send(content);
         }
      };

   /**
    * An internal function to handle an Object argument, which may contain
    * either AjaxRequest field values or parameter name/values
    */
   req.handleArguments = 
      function(args) {
         for (var i in args) {
            // If the AjaxRequest object doesn't have a property which was passed, treat it as a url parameter
            if (typeof(req[i]) == "undefined") {
               req.parameters[i] = args[i];
            }
            else {
               req[i] = args[i];
            }
         }
      };

   /**
    * Returns the results of XMLHttpRequest.getAllResponseHeaders().
    * Only available after a response has been returned
    */
   req.getAllResponseHeaders =
      function() {
         if (req.xmlHttpRequest != null) {
            if (req.responseReceived) {
               return req.xmlHttpRequest.getAllResponseHeaders();
            }

            alert("Cannot getAllResponseHeaders because a response has not yet been received");
         }
      };

   /**
    * Returns the the value of a response header as returned by 
    * XMLHttpRequest,getResponseHeader().
    * Only available after a response has been returned
    */
   req.getResponseHeader =
      function(headerName) {
         if (req.xmlHttpRequest != null) {
            if (req.responseReceived) {
               return req.xmlHttpRequest.getResponseHeader(headerName);
            }

            alert("Cannot getResponseHeader because a response has not yet been received");
         }
      };

   return req;
}

// ---------------------------------------
// Static methods of the AjaxRequest class
// ---------------------------------------

/**
 * Returns an XMLHttpRequest object, either as a core object or an ActiveX 
 * implementation. If an object cannot be instantiated, it will return null;
 */
AjaxRequest.getXmlHttpRequest = function() {
   try {
      var request = new XMLHttpRequest();
      return request;
   } catch (tryMicrosoft) {
      if (window.ActiveXObject) {
         var aVersions = ["MSXML2.XMLHttp.5.0", "MSXML2.XMLHttp.4.0", "MSXML2.XMLHttp.3.0",
                          "MSXML2.XMLHttp", "Microsoft.XMLHttp"];

         for (var i = 0; i < aVersions.length; i++) {
            try {
               var oXmlHttp = new ActiveXObject(aVersions[i]);
               return oXmlHttp;
            } catch (oError) {
               // Do nothing
            }
         }
      }
   }

   return null;
};

/**
 * See if any request is active in the background
 */
AjaxRequest.isActive = function() {
   return (AjaxRequest.numActiveAjaxRequests > 0);
};

/**
 * Make a GET request. Pass an object containing parameters and arguments as 
 * the second argument.
 * These areguments may be either AjaxRequest properties to set on the request 
 * object or name/values to set in the request querystring.
 */
AjaxRequest.get = function(args) {
   AjaxRequest.doRequest("GET", args);
};

/**
 * Make a POST request. Pass an object containing parameters and arguments as 
 * the second argument.
 * These areguments may be either AjaxRequest properties to set on the request 
 * object or name/values to set in the request querystring.
 */
AjaxRequest.post = function(args) {
   AjaxRequest.doRequest("POST",args);
};

/**
 * The internal method used by the .get() and .post() methods
 */
AjaxRequest.doRequest = function(method, args) {
   if (typeof(args) != "undefined" && args != null) {
      var myRequest = new AjaxRequest();

      myRequest.method = method;
      myRequest.handleArguments(args);
      myRequest.process();
   }
};

/**
 * Submit a form. The requested URL will be the form's ACTION, and the request 
 * method will be the form's METHOD.
 * Returns true if the submittal was handled successfully, else false so it 
 * can easily be used with an onSubmit event for a form, and fallback to 
 * submitting the form normally.
 */
AjaxRequest.submit = function(theform, args) {
   var myRequest = new AjaxRequest();
   if (myRequest==null) { 
      return false; 
   }

   var serializedForm = AjaxRequest.serializeForm(theform);
   myRequest.method = theform.method.toUpperCase();
   myRequest.url = theform.action;
   myRequest.handleArguments(args);
   myRequest.queryString = serializedForm;
   myRequest.process();

   return true;
};

/**
 * Serialize a form into a format which can be sent as a GET string or a POST 
 * content.It correctly ignores disabled fields, maintains order of the fields 
 * as in the elements[] array. The 'file' input type is not supported, as 
 * its content is not available to javascript. This method is used internally
 * by the submit class method.
 */
AjaxRequest.serializeForm = function(theform) {
   var els = theform.elements;
   var len = els.length;
   var queryString = "";
   this.addField = 
      function(name, value) { 
         if (queryString.length > 0) { 
            queryString += "&";
         }

         queryString += encodeURIComponent(name) + "=" + encodeURIComponent(value);
      };
   for (var i = 0; i < len; i++) {
      var el = els[i];
      if (!el.disabled) {
         switch(el.type) {
            case 'text': case 'password': case 'hidden': case 'textarea': 
               this.addField(el.name, el.value);
               break;
            case 'select-one':
               if (el.selectedIndex >= 0) {
                  this.addField(el.name, el.options[el.selectedIndex].value);
               }
               break;
            case 'select-multiple':
               for (var j = 0; j < el.options.length; j++) {
                  if (el.options[j].selected) {
                     this.addField(el.name, el.options[j].value);
                  }
               }
               break;
            case 'checkbox': case 'radio':
               if (el.checked) {
                  this.addField(el.name, el.value);
               }
               break;
         }
      }
   }

   return queryString;
};

// -----------------------
// Static Class variables
// -----------------------

/**
 * The number of total AjaxRequest objects currently active and running
 */
AjaxRequest.numActiveAjaxRequests = 0;

/**
 * An object holding the number of active requests for each group
 */
AjaxRequest.numActiveAjaxGroupRequests = new Object();

/**
 * The total number of AjaxRequest objects instantiated
 */
AjaxRequest.numAjaxRequests = 0;


