/**
 * Wrapper object for XMLHttpRequest, that implements most of the tasks
 * common to all asynchronous HTTP requests. The only function that requires
 * implementation is onComplete, which this object calls once the response
 * the server has been received. 
 *
 * @param   mimeType    Optional, defaults to XML if blank or not 'TEXT'.
 *                      Case-insensitive string that specifies the mime-type
 *                      of the server response. Accepted values are 'XML' and
 *                      'TEXT'. This is only for the purpose of determining
 *                      whether XMLHttpRequest.overrideMimeType should be set
 *                      to 'text/xml'.
 *
 *                      
 */
function HttpRequest(mimeType) {
    this.mimeType = (mimeType) ? mimeType.toUpperCase() : 'XML';
    this.async = true;
    if (this.mimeType != 'TEXT') {
        this.mimeType = 'XML';
    }
    
    this.requestObj = this.createRequestObj();
    
}

/**
 * Allow send url to be dynamically scripted
 * Finds the url part, before searchStr
 */
function getBaseURL(searchStr) {
    var rewriteURL = top.location.href;
    
    var urlDiv = rewriteURL.indexOf(searchStr);
    var proto = rewriteURL.substring(0, rewriteURL.indexOf(':'));
    rewriteURL = proto + "://"
        + rewriteURL.substring(rewriteURL.indexOf('://') + 3, urlDiv);
        
    return rewriteURL;
}

/**
 * Creates the XMLHttpRequest object.
 *
 * Called by the constructor.
 */
HttpRequest.prototype.createRequestObj =
function() {
    var requestObj;
    
    try {
        // Firefox, Opera 8.0+, Safari, IE 7+
        requestObj = new XMLHttpRequest();
        if (this.mimeType == 'XML' && requestObj.overrideMimeType) {
            requestObj.overrideMimeType('text/xml');
        }
    } catch(e) {
        try {
            // IE 6.0, before IE 7
            requestObj = new ActiveXObject('Msxml2.XMLHTTP');
        } catch (e) {
            try {
                // IE 5.5+, before 6.0
                requestObj = new ActiveXObject('Microsoft.XMLHTTP');
            } catch (e) {
                //Browser doesn't support AJAX
                return false;
            }
        }
    }

    //If we got here, object was created successfully
    return requestObj;
}

HttpRequest.prototype.setAsync =
function(async) {
    this.async = async;
}

/**
 * Tells whether AJAX is supported.
 *
 * If AJAX is not supported, and form data must be sent to the server,
 * then a fallback method should call [form].submit().
 */
HttpRequest.prototype.supported =
function() {
    if (!this.requestObj) {
        return false;
    }
    
    return true;
}

/**
 * Allow send url to be dynamically scripted
 */
function getBaseURL(searchStr) {
    var rewriteURL = top.location.href;
    var urlDiv = rewriteURL.indexOf(searchStr) + searchStr.length;
    rewriteURL = "http://"
        + rewriteURL.substring(rewriteURL.indexOf('://') + 3, urlDiv);
}

/**
 * Send a request to the server.
 *
 * @param   url     Optional--the url to send to; if not specified,
 *                  the url for the current script is used, including
 *                  any query information (the part of url after '?').
 *
 *                  If this is the only parameter, it is possible to build
 *                  the query manually. In this case, it is recommended to
 *                  the QueryBuilder. This is especially useful if you
 *                  only want to partial form data, or some custom data not
 *                  in a form.
 *
 * @param   method  Optional--the method to use, case insensitive. If not
 *                  specified, defaults to 'GET'. Currently, 'GET' and 'POST'
 *                  are the only methods supported. To use other methods,
 *                  such as PUT, DELETE, or HEAD, etc., you must use
 *                  XMLHttpRequest directly.
 *
 * @param   form    Optional--the form object that contains the data that
 *                  we want to send to the server. If this is provided, then
 *                  all the form data will be processed and sent, using the
 *                  specified method. The form data is formatted in the exact
 *                  same way that the browser would format it.
 *
 * See also: QueryBuilder
 */
HttpRequest.prototype.send =
function(url, method, form) {
    //If no url is specified, then assume we are requesting from same file
    if (!url) {
        url = window.location.pathname + window.location.search;
    }
    
    //If a method is provided, convert it to upper case, otherwise default to 'GET'
    method = (method) ? method.toUpperCase() : 'GET';
    if (method != 'POST') {
        method = 'GET';
    }
    
    var formData = '';
    
    if (form) {
        /* Calculate where the last element with a name is,
         * so we know where to stop appending '&';
         */
        var endMarker = 1;
        for (var i = 0; i < form.elements.length; i++) {
            var eName = form.elements[i].name;
            if (!eName || eName == '') {
                endMarker++;
            }
        }
        
        for (var i = 0; i < form.elements.length; i++) {
            var eName = form.elements[i].name;
            var eValue = form.elements[i].value;
            
            if (eName && eName != '') {
                eName = escape(eName)
                    .replace(new RegExp('\\+', 'g'), '%2b');
                eValue = escape(eValue)
                    .replace(new RegExp('\\+', 'g'), '%2b');

                formData += eName + '=' + eValue;
                if (i != form.elements.length - endMarker) {
                    formData = formData + '&';
                }
            }
        }
    }
    
    if (this.requestObj) {
        if (this.requestObj.readyState == 0 || this.requestObj.readyState == 4) {

            /* Closure is necessary here, so that we can call onReadyStateChange below.
             * Calling this.onReadyStateChange() would cause an error.
             */
            var selfRef = this;
            this.requestObj.onreadystatechange = function() { selfRef.onReadyStateChange(); };
            
            if (method == 'GET' && formData) {
                url += '?' + formData;
            }
            
            this.requestObj.open(method, url, this.async);
            this.requestObj.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
            
            if (method == 'GET') {
                this.requestObj.send(null);
            } else {
                this.requestObj.setRequestHeader('Content-length', formData.length);
                this.requestObj.send(formData);
            }
            
        }
    }
}

/**
 * Abort the request.
 */
HttpRequest.prototype.abortCallBack =
function() {
    if (this.requestObj) {
        this.requestObj.abort();
    }
}

/**
 * Called when the request is initialized, but has not yet been sent.
 *
 * To be implemented by the user.
 */
HttpRequest.prototype.onInited = function() {}

/**
 * Called when send() is invoked on the request, and
 * headers and status are available.
 *
 * Warning: Mozilla and Microsoft implementations do not call back
 * on this state change until XMLHttpRequest.readyState is 4 (complete)
 *
 * To be implemented by the user.
 */
HttpRequest.prototype.onSend = function() {}

/**
 * Called as soon as XMLHttpRequest.responseText and XMLHttpRequest.responseXML
 * begin to hold partial data.
 *
 * Warning: Mozilla and Microsoft implementations do not call back
 * on this state change until XMLHttpRequest.readyState is 4 (complete)
 *
 * To be implemented by the user.
 */
HttpRequest.prototype.onDownloading = function() {}

/**
 * Called when all operations of the request have completed, and the data is
 * fully received.
 * 
 * To be implemented by the user.
 */
HttpRequest.prototype.onComplete = function(responseText, responseXML) {}

/**
 * Called when the request is aborted.
 * 
 * To be implemented by the user.
 */
HttpRequest.prototype.onAbort = function() {}

/**
 * Called when an error status was received from the server,
 * in response to the request.
 * 
 * To be implemented by the user.
 *
 * @param   status       Status code sent from the server
 *
 * @param   statusText   Text message that is sent from the server.
 *                       This is the text equivalent of the status code.
 *
 * @param   responseText The text that was received from the server, if any.
 */
HttpRequest.prototype.onError = function(status, statusText, responseText) {}

/**
 * Request handler.
 */
HttpRequest.prototype.onReadyStateChange =
function() {
    if (this.requestObj.readyState == 1) {
        this.onInited();
    } else if (this.requestObj.readyState == 2) {
        this.onSend();
    } else if (this.requestObj.readyState == 3) {
        this.onDownloading();
    } else if (this.requestObj.readyState == 4) {
        try {
            if (this.requestObj.status == 0) {
                this.onAbort();
            } else if (this.requestObj.status == 200 && this.requestObj.statusText == "OK") {
                this.onComplete(
                    this.requestObj.responseText,
                    this.requestObj.responseXML
                );
            } else {
                this.onError(
                    this.requestObj.status,
                    this.requestObj.statusText,
                    this.requestObj.responseText
                );
            }
        } catch (e) {
            alert(e);
        }
    }
}

/**
 * Get an array of values from the specified tag name.
 
 * @param   doc         XML document
 * @param   tagName     name of the element that contains the data
 */
function getXMLValues(doc, tagName) {
    var arr = [];
    list = doc.getElementsByTagName(tagName);
    for (var i = 0; i < list.length; i++) {
        arr[i] = list.item(i).firstChild.data;
    }
    
    return arr;
}

/**
 * Get the contents of an XML element in the XML document sent from the server.
 *
 * @param   doc         XML document
 * @param   tagName     name of the element that contains the data
 */
function getXMLData(doc, tagName) {
    list = doc.getElementsByTagName(tagName);
    elem = list.item(0);
    child = elem.firstChild;
    if (child != null) {
        return child.data;
    } else {
        return null;
    }
}

/**
 * Get the contents of an XML element in the XML document sent from the server.
 * For example: <Transaction ID="87223">Deposit</Transaction>
 * will result in 'Deposit'.
 *
 * @param   doc         XML document
 * @param   tagName     name of the element that contains the data (Transaction)
 * @param   attName     name of the attribute whose value we also specify (ID)
 * @param   attValue    the attribute's value (87223)
 */
function getXMLDataSpec(doc, tagName, attName, attValue) {
    list = doc.getElementsByTagName(tagName);
    for (var i = 0; i < list.length; i++) {
        elem = list.item(i);
        if (elem.getAttribute(attName) == attValue) {
            return elem.firstChild.data;
        }
    }
}

/**
 * Get the value of an XML element's attribute in the XML document sent from the server.
 *
 * @param   doc         XML document
 * @param   tagName     name of the element whose attribute we want to get
 * @param   attName     name of the attribute
 */
function getXMLAttribute(doc, tagName, attName) {
    list = doc.getElementsByTagName(tagName);
    elem = list.item(0);

    return elem.getAttribute(attName);
}

/**
 * Convenience Object for building a URL query. This is useful if
 * you want to send data that does not come from a web form.
 *
 * @param   url     url of the script to call
 */
function QueryBuilder(url) {
    this.url = url;
    this.paramsAdded = 0;
    
    if (!url) {
        throw new Error("url must be specified");
    }
}

/**
 * Add a name-value pair to the parameter list of this query. The
 * data is formatted in the same exact way that the browser would
 * format it.
 */
QueryBuilder.prototype.add =
function (eName, eValue) {
    if (this.paramsAdded == 0) {
        this.url += '?';
    } else {
        this.url += '&';
    }
    
    if (eName && eName != '') {
        eName = escape(eName)
            .replace(new RegExp('\\+', 'g'), '%2b');
        eValue = escape(eValue)
            .replace(new RegExp('\\+', 'g'), '%2b');
        
        this.url += eName + '=' + eValue;
        this.paramsAdded = 1;
    }
}
