/*
 * ajaxstub.js
 *
 * Written by: Carl J. Nobile
 *
 * $Author: cnobile $
 * $Date: 2008-11-24 16:24:18 $
 * $Revision: 1.9 $
 */

window.onunload = function() {
  delete AjaxStub;
}

Function.prototype.bind = function(object) {
  var method = this;
  return function () {
    method.apply(object, arguments);
  };
}

// Ajax request object.
var AjaxStub = (function() {
  var __EXCEPTION_CB = "exceptionCB";
  var __LOGGING_CB = "loggingCB";
  var __TEST_CB = "textCB";
  var __CONTINUE_CB = "continueCB";
  var __NO_CONTENT_CB = "noContentCB";

  var constructorFn = function(url, contentType, funcList) {
    var self = this.AjaxStub;
    self.__url = url;
    self.__contentType = contentType;
    // {datetime: request}
    self.__requests = new Object();
    // {callback name: [object, [datetime, datetime, ...]]}
    self.__handlers = new Object();
    self.__stubs = new Object();
    self.register(funcList);
  };

  constructorFn._findStubs = function(stubs, suffix, obj) {
    var map = new Object();

    // Walk the scope in obj looking for the stubs.
    function walk(stubs, suffix, map, obj) {
      for(var i = 0; i < stubs.length; i++) {
        map[stubs[i] + suffix] = obj[stubs[i] + suffix];
      }
    }

    walk(stubs, suffix, map, window);

    if(obj) {
      walk(stubs, suffix, map, obj);
    }

    return map;
  };

  constructorFn.register = function(list, obj) {
    var map = this._findStubs(list, "CB", obj);

    for(var key in map) {
      this.__handlers[key] = [map[key], []];
    }

    map = this._findStubs(list, "", obj);

    for(var key in map) {
      this.__stubs[key] = map[key];
    }
  };

  constructorFn.unregister = function(list) {
    for(var i = 0; i < list.length; i++) {
      delete this.__handlers[list[i]];
      delete this.__stubs[list[i]];
    }
  };

  constructorFn.getStubFunction = function(name) {
    var method = null;

    try {
      method = this.__stubs[name];
    } catch (e) {
      exceptionCB("Invalid stub method: " + name);
    }

    return method;
  };

  constructorFn.log = function(value) {
    if(window["debug"] != undefined && debug) {
      this.__handlers[__LOGGING_CB][0](value);
    }
  };

  // AJAX request
  constructorFn._ajaxRequest = function() {
    var request = null;

    try {
      request = new XMLHttpRequest();

      if(request.overrideMimeType) {
        request.overrideMimeType(this.__contentType);
      }
    } catch (e) {
      try { request = new ActiveXObject("Msxml2.XMLHTTP");
      } catch (e) {
        try { request = new ActiveXObject("Microsoft.XMLHTTP");
        } catch (e) {
        }
      }
    }

    return request;
  };

  // Execute stub
  constructorFn.executeStub = function(name, cgiMethod, args) {
    var query = "";
    var url = this.__url;
    var request = this._ajaxRequest();
    var key = new Date().getTime();
    this.__requests[key] = request;
    var ex_key = this.__handlers[__EXCEPTION_CB][1];
    ex_key.push(key);
    var cb_key = this.__handlers[name + "CB"][1];
    cb_key.push(key);
    //self.log("Number of request keys: " + self.__requests.toSource());

    if(request == null) {
      this._processCallback(__EXCEPTION_CB, "Invalid request object.", key);
      return;
    }

    if(cgiMethod == "GET") {
      query += url.match(/\?/) ? "&" : "?";
    }

    // TODO: Add a security tag
    query += "cmd=" + escape(name);

    for(var i = 0; i < args.length; i++) {
      query += "&arg=" + escape(args[i]);
    }

    // Fix the IE caching bug.
    query += "&id=" + escape(key);

    if(cgiMethod == "GET") {
      url += query;
      query = null;
    }

    this.log(url);

    if(query) {
      this.log(query);
    }

    request.onreadystatechange = this._handleResponse.bind(this);
    request.open(cgiMethod, url, true);

    if(cgiMethod == "POST") {
      request.setRequestHeader("Content-type",
                               "application/x-www-form-urlencoded");

      // Fix a mozilla bug.
      if(request.overrideMimeType) {
        request.setRequestHeader("Connection", "close");
      }
    }

    request.send(query);
  };

  constructorFn._processCallback = function(callback, value, key) {
    if(window["debug"] != undefined && debug) {
      var receiveDate = new Date().getTime();
      var index = this._getHandlerRequestKeyIndex(callback, key);
      var sendDate = this.__handlers[callback][1][index];
      this.log("Round trip time for process id: " + key + "--" + callback +
               ": " + (receiveDate - sendDate) + "ms");
    }

    //this.log("Processed request key: " + key + ", index: " + index);
    this.__handlers[callback][1].splice(index, 1);
    this.__handlers[__EXCEPTION_CB][1].splice(index, 1);
    this.__handlers[callback][0](value);
  };

  constructorFn._getHandlerRequestKeyIndex = function(callback, key) {
    var cb_key = this.__handlers[callback][1];

    for(var i = 0; i < cb_key.length; i++) {
      if(cb_key[i] == key) {
        return i;
      }
    }
  }

  // Handle request
  constructorFn._handleResponse = function() {
    for(var key in this.__requests) {
      var request = this.__requests[key];
      //self.log("Ready state for request [" + key + "] is: " + request.readyState);

      if(request != null && request.readyState == 4) {
        var status = request.status;
        var response = null;

        if(status == 100) {
          delete this.__requests[key];
          this._processCallback(__CONTINUE_CB, null, key);
        } else if(status == 204) {
          delete this.__requests[key];
          this._processCallback(__NO_CONTENT_CB, null, key);
        } else if(status == 200 || status == 201) {
          delete this.__requests[key];
          this._processResponse(request, key);
        } else {
          response = "Error while retrieving result from the server: " +
                     (status ? "(" + status : "undefined") + ") " +
                     request.statusText;
          this.log(response);
          delete this.__requests[key];
          this._processCallback(__EXCEPTION_CB, response, key);
        }
      }
    }
  };

  constructorFn._processResponse = function(request, key) {
    try {
      var cType = request.getResponseHeader("Content-Type");
      var jsonFlag = cType.toLowerCase() == "text/x-json" ? true : false;
      var xmlFlag =  cType.toLowerCase() == "text/xml" ? true : false;

      if(jsonFlag) {
        this._parseJSON(request.responseText, key);
      } else if(xmlFlag) {
        var response = request.responseText;

        if(request.responseXML) {
          response = request.responseXML;
        }

        this._parseXML(response, key);
      } else {  // Assume the Content-Type: text/html.
        this._parseText(request.responseText, key);
      }
    } catch (e) {
      this._processCallback(__EXCEPTION_CB, "Caught error: " + e + ", " +
                                            request.responseText + ", " + key);
    }
  };

  constructorFn._parseJSON = function(json, key) {
    if(/^("(\\.|[^"\\\n\r])*?"|[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t])+?$/
      .test(json)) {
      json = eval("(" + json + ")");
      var type = json ? json["status"] : "failure";
      var response = null;
      var callback = null;

      if("failure" == type.toLowerCase()) {
        response = json ? json["result"] : "No JSON response";
        this._processCallback(__EXCEPTION_CB, response, key);
      } else {
        var stubList = json["result"];
        callback = stubList[0];
        response = stubList[1];
        this._processCallback(callback, response, key);
      }
    } else {
      this.log("Invalid JSON Request: " + json);
    }
  };

  constructorFn._parseText = function(text, key) {
    text = text ? text.replace(/^\s*|\s*$/g, "") : "No TEXT response";
    this._processCallback(__TEST_CB, text, key);
  };

  constructorFn._parseXML = function(xml, key) {
    if(!xml || !xml.childNodes) {
      xml = "No XML response";
      this._processCallback(__EXCEPTION_CB, xml, key);
      return;
    }

    var root = xml.documentElement;

    if(root.childNodes.length > 0) {
      var status = root.attributes.getNamedItem("status").value;

      if(status == "failure") {
        var value = this.getNodeText(root);
        this._processCallback(__EXCEPTION_CB, value, key);
      } else {
        var nodes = root.childNodes;
        var callback = null;
        var value = null;

        for(var i = 0; i < nodes.length; i++) {
          if(nodes[i].nodeType == 1) {
            callback = nodes[i].nodeName;
            value = this.getNodeText(nodes[i]);
            this._processCallback(callback, value, key);
          }
        }
      }
    }
  };

  constructorFn.getNodeText = function(doc) {
    var out = null;
    var nodes = doc.childNodes;

    for(var i = 0; i < nodes.length; i++) {
      if(nodes[i].nodeType == 3 || nodes[i].nodeType == 4) {
        if(out == null) {
          out = nodes[i].data.replace(/^\s*|\s*$/g, "");
        } else {
           out += nodes[i].data.replace(/^\s*/g, " ").replace(/\s*$/g, "");
        }
      }
    }

    return out;
  };

  return constructorFn;
})();
