// CacheManager
function CacheManager() {

  var self = this;

  // data storage objects
  self.initdata = function() {
    // Container object for all data
    self.data = new Object();
      self.data.latest_dbtime = 0;
      // TESTRESULTS
      self.data.testresults = new Object();
        var d = new Date(0);
        self.data.testresults.last_update = d.getTime() / 1000.0;
        self.data.testresults.urlidmap = new Object();  // urlidmap[urlid][clients] = arr
        self.data.testresults.clientmap = new Object(); // clientmap[clients][urlid] = arr
                                                        // arr is an array of objects.
                                                        // each object has these properties:
                                                        // x.testresultsid, x.urlid, x.rtt_avg
                                                        // x.rtt_min, x.rtt_max, x.clients
                                                        // x.retcode, x.count, x.logtime
      // OBJECTS
      self.data.objects = new Object();
        self.data.objects.urlidmap = new Object(); // urlidmap[urlid] = x
      // ACTIVITYLOG
      self.data.activitylog = new Object();
        self.data.activitylog.latest_time = 0;
        self.data.activitylog.arr = new Array(); // each element is an object with these properties:
                                                 // ob.ownerid, ob.added, ob.type, ob.content
      // NEWS
      self.data.news = new Object();
        self.data.news.arr = new Array(); // each element is an object with these properties:
                                          // ob.newsid, ob.added, ob.title, ob.content

      // TESTLOG
      self.data.testlog = new Object(); // one object containing all the testlog variables:
                                        // ob.testlogid, ob.starttime, ob.endtime, ob.ownerid,
                                        // ob.testname, ob.startclients, ob.stopclients, 
                                        // ob.stepclients, ob.timeout, ob.source, ob.status,
                                        // ob.targetdomain, ob.testtype, ob.comment, ob.updated
                                        // ob.testconfigid
      // LIVE FEEDBACK
      self.data.livefeedback = new Object();
        self.data.livefeedback.latest_id_seen = 0;
        self.data.livefeedback.arr = new Array(); // Array with all historical live_feedback values
                                                  // arr[x] = Object:
                                                  // ob.id, ob.testlogid, ob.updated, ob.tx_bytes
                                                  // ob.rx_bytes, ob.progress_text, ob.clients_active
                                                  // ob.get_requests, ob.post_requests 
                                                  // ob.connections_active, ob.clients
                                                  // ob.progress_total, ob.progress_subtest
      // SITE STATISTICS
      self.data.sitestatistics = new Object(); // ob.clients, ob.connections, ob.bytes_per_second

  }


  // PUBLIC API - CALL THESE FUNCTIONS TO CONTROL/ACCESS CACHE MANAGER

  // clearrequests() - Deletes all requests from the updater object 
  self.clearrequests = function() {
    // self.updater.requests = null;
    self.updater.requests = new Object();
    self.updater.requests_active = new Object();
  }

  // clearrequest() - Deletes one request from the updater object
  self.clearrequest = function(request_str) {
    if (self.updater.requests[request_str] != undefined) {
      // self.updater.requests[request_str] = undefined;
      delete self.updater.requests[request_str];
    }
  }

  // setrequest() - Sets one request in the updater object
  // request_str = "testresults"
  // request_args = Object:
  //   request_args.time = 3, request_args.limit = 2
  self.setrequest = function(request_str, request_args) {
    self.updater.requests[request_str] = request_args;
  }

  // setcallback() - sets a callback function for when data changes
  self.setcallback = function(request_str, callback) {
    self.data[request_str].callback = callback;
  }

  // modifyrequest() - Changes request parameters
  //   Modifies specified arguments in an existing request
  //   Note that it does not touch existing arguments if they are not
  //   included in the request_args list
  self.modifyrequest = function(request_str, request_args) {
    if (self.updater.requests[request_str] != undefined) {
      for (arg in request_args) {
        self.updater.requests[request_str][arg] = request_args[arg];
      }
    }
  }

  // setrefresh() - Sets refresh interval (seconds)
  self.setrefresh = function(refresh_interval) {
    self.updater.refreshtime = refresh_interval * 1000;
  }

  // stoprefresh() - stops refresh operations
  self.stoprefresh = function() {
    if (self.updater.callout != undefined) {
      clearTimeout(self.updater.callout);
      delete self.updater.callout;
    }
    if (self.updater.xhReq != undefined) {
      self.updater.xhReq.abort();
      delete self.updater.xhReq;
    }
  }

  // startrefresh() - (re)starts refresh operations
  self.startrefresh = function() {
    // if running, stop it
    self.stoprefresh();
    // then (re)start
    self.updater.refresh();
  }

  // END PUBLIC API FUNCTIONS - DO NOT CALL BELOW FUNCTIONS DIRECTLY


  // Updater object
  // This is the object that performs AJAX requests
  self.initupdater = function() {
    self.updater = new Object();
    // Updater private method _set_testresult
    self.updater._set_testresult = function(ob, prop, result_arr) {
      var retcode = result_arr[6];
      var set_ob = undefined;
      if (ob[prop] == undefined) {
        ob[prop] = new Array();
      }
      else {
        // try to see if this testresult already exists (for update)
        // check if we have an array entry (object) with the same retcode
        var arrlen = ob[prop].length;
        for (var i = 0; i < arrlen; i++) {
          if (ob[prop][i].retcode == retcode) {
            set_ob = ob[prop][i];
            break;
          }
        }
      }
      if (set_ob == undefined) {
        set_ob = new Object();
        ob[prop].push(set_ob);
      }
      set_ob.testresultsid = result_arr[0];
      set_ob.urlid = result_arr[1];
      set_ob.rtt_avg = result_arr[2];
      set_ob.rtt_min = result_arr[3];
      set_ob.rtt_max = result_arr[4];
      set_ob.clients = result_arr[5];
      set_ob.retcode = result_arr[6];
      set_ob.count = result_arr[7];
      set_ob.logtime = result_arr[8];
    }
    // Updater private method _progressCallback
    self.updater._progressCallback = function() {
      var xhReq = self.updater.xhReq;
      if (xhReq.readyState != 4)
        return;
      if (xhReq.status < 200 || xhReq.status > 299)
        return;
      var responses = xhReq.responseText.split("\n");
      var responses_length = responses.length;
      if (responses_length < 3)
        return;
        
      // Iterate through responses
      for (var i = 0; i < responses_length; i++) {
        var request_response = responses[i].split(":");
        if (request_response.length < 2)
          continue;
        var request = request_response[0];
        var response = request_response[1];
        if (response.length < 1)
          continue;
        // ignore response 0 and 1, but save dbtime response (1)
        if (i < 2 || request == "elapsed-time") {
          if (i == 1)
            self.data.latest_dbtime = response;
          continue;
        }
        
        response = Base64.decode(response);
        
        switch (request) {
          case 'objects':
            var objects = response.split("\n");
            var object_count = objects.length;
            var objects_out = self.data.objects;
            var got_something = false;
            for (var j = 0; j < object_count; j++) {
              if (objects[j].length < 3)
                continue;
              var object_parts = objects[j].split(",");
              if (object_parts.length < 2)
                continue;
              var urlid = object_parts[0];
              got_something = true;
              objects_out.urlidmap[urlid] = self._fromHex(object_parts[1]);
            }
            if (got_something) {
              // modify next request to only ask for later urls
              // Here we use latest_dbtime that we should have gotten from get.php before any other responses
              self.modifyrequest("objects", { "fromtime" : "" + self.data.latest_dbtime });
              // call callback if we have one
              if (self.data.objects.callback != undefined) {
                // alert("Calling objects callback");
                self.data.objects.callback();
              }
            }
            break;
          case 'testresults':
            var results = response.split("\n");
            var results_count = results.length;
            var results_out = self.data.testresults;
            var got_something = false;
            for (var j = 0; j < results_count; j++) {
              if (results[j].length < 10)
                continue;
              var results_parts = results[j].split(",");
              if (results_parts.length < 9)
                continue;
              var testresultid = results_parts[0];
              var urlid = results_parts[1];
              var clients = results_parts[5];
              var logtime = results_parts[8];
              got_something = true;
              if (logtime > results_out.last_update)
                results_out.last_update = logtime;
              if (results_out.urlidmap[urlid] == undefined)
                results_out.urlidmap[urlid] = new Object();
              self.updater._set_testresult(results_out.urlidmap[urlid], clients, results_parts);
              if (results_out.clientmap[clients] == undefined)
                results_out.clientmap[clients] = new Object();
              self.updater._set_testresult(results_out.clientmap[clients], urlid, results_parts);
            }
            if (got_something) {
              // modify next request to only ask for later results
              self.modifyrequest("testresults", { "fromtime" : "" + self._nextTime(results_out.last_update) } );
              if (self.data.testresults.callback != undefined) {
                // alert("Calling testresults callback");
                self.data.testresults.callback();
              }
            }
            break;
          case 'activitylog':
            var results = response.split("\n");
            var results_count = results.length;
            var results_out = self.data.activitylog;
            var latest_time = 12345;
            for (var j = 0; j < results_count; j++) {
              if (results[j].length < 7)
                continue;
              var results_parts = results[j].split(",");
              if (results_parts.length < 4)
                continue;
              var ob = new Object();
              ob.ownerid = results_parts[0];
              ob.added = results_parts[1];
              ob.type = results_parts[2];
              ob.content = Base64.decode(results_parts[3]);
              ob.testlogid = results_parts[4];
              ob.activitylogid = results_parts[5];
			  ob.user_time = results_parts[6];
              if (ob.added > latest_time) {
                results_out.latest_time = latest_time = ob.added;                
              }
              results_out.arr.push(ob);
            }
            if (latest_time > 0) {
              self.modifyrequest("activitylog", { "fromtime" : latest_time });
              if (self.data.activitylog.callback != undefined) {
                // alert("Calling activitylog callback");
                self.data.activitylog.callback();
              }
            }
            break;
          case 'sitestatistics':
            var results = response.split("\n");
            var results_count = results.length;
            var results_out = self.data.sitestatistics;
            var tmpobs = new Object();
            for (var j = 0; j < results_count; j++) {
              var results_parts = results[j].split(",");
              if (results_parts.length < 6)
                continue;
              // store the data in an object that we then store in our tmpobs object
              // we do this to get rid of duplicate reports, for the same testlogid
              var testlogid = results_parts[2];
              var tmpob = new Object();
              tmpob.clients = results_parts[3];
              tmpob.connections = results_parts[4];
              tmpob.bytes_per_second = results_parts[5];
              tmpobs[testlogid] = tmpob;
            }
            // now we have a tmpobs object containing a property for each testlogid. Those
            // properties, in turn, point to objects containing the most recent data for 
            // each testlogid. Now we just need to add it all together
            results_out.clients = 0;
            results_out.connections = 0;
            results_out.bytes_per_second = 0;
            for (ob in tmpobs) {
              results_out.clients += ob.clients;
              results_out.connections += ob.connections;
              results_out.bytes_per_second += ob.bytes_per_second;
            }
            results_out.callback();
            break;
          case 'livefeedback':
            var results = response.split("\n");
            var results_count = results.length;
            var results_out = self.data.livefeedback;
            var highest_id_seen = 0;
            for (var j = 0; j < results_count; j++) {
              var results_parts = results[j].split(",");
              if (results_parts.length < 13)
                continue;
              var ob = new Object();
              ob.id = results_parts[0];
              ob.testlogid = results_parts[1];
              ob.updated = results_parts[2];
              ob.tx_bytes = results_parts[3];
              ob.rx_bytes = results_parts[4];
              ob.progress_text = Base64.decode(results_parts[5]);
              ob.clients_active = results_parts[6];
              ob.get_requests = results_parts[7];
              ob.post_requests = results_parts[8];
              ob.connections_active = results_parts[9];
              ob.clients = results_parts[10];
              ob.progress_total = results_parts[11];
              ob.progress_subtest = results_parts[12];
              results_out.arr.push(ob);
              if (ob.id > highest_id_seen)
                highest_id_seen = ob.id;
            }
            if (highest_id_seen > 0) {
              self.modifyrequest("livefeedback", { "fromid" : highest_id_seen });
              if (results_out.callback != undefined) {
                // alert("Calling livefeedback callback");
                results_out.callback();
              }
            }
            break;
          case 'testlog':
            var results_out = self.data.testlog;
            var results_parts = response.split(",");
            if (results_parts.length < 17)
              break;
            results_out.testlogid = results_parts[0];
            results_out.starttime = results_parts[1];
            results_out.endtime = results_parts[2];
            results_out.ownerid = results_parts[3];
            results_out.testname = Base64.decode(results_parts[4]);
            results_out.startclients = results_parts[5];
            results_out.stopclients = results_parts[6];
            results_out.stepclients = results_parts[7];
            results_out.timeout = results_parts[8];
            results_out.source = Base64.decode(results_parts[9]);
            results_out.status = results_parts[10];
            results_out.targetdomain = Base64.decode(results_parts[11]);
            results_out.testtype = results_parts[12];
            results_out.comment = Base64.decode(results_parts[13]);
            results_out.testconfigid = results_parts[14];
            results_out.updated = results_parts[15];
            results_out.timequeued = results_parts[16];
            self.modifyrequest("testlog", { "fromtime" : self._nextTime(results_out.updated) });
            if (self.data.testlog.callback != undefined) {
              // alert("Calling testlog callback");
              self.data.testlog.callback();
            }
            break;
          case 'news':
            var results = response.split("\n");
            var results_count = results.length;
            var results_out = self.data.news;
            var highest_newsid = 0;
            for (var j = 0; j < results_count; j++) {
              if (results[j].length < 9)
                continue;
              var results_parts = results[j].split(",");
              if (results_parts.length < 4)
                continue;
              var ob = new Object();
              ob.newsid = results_parts[0];
              ob.added = results_parts[1];
              ob.title = Base64.decode(results_parts[2]);
              ob.content = Base64.decode(results_parts[3]);
              results_out.arr.push(ob);
              if (ob.newsid > highest_newsid)
                highest_newsid = ob.newsid;
            }
            if (highest_newsid > 0) {
              self.modifyrequest("news", { "fromid" : highest_newsid });
              if (self.data.news.callback != undefined) {
                // alert("Calling news callback");
                self.data.news.callback();
              }
            }
            break;            
        }
      }      
      // call refresh() again
      if (--self.updater.refreshcount != 0)
        self.updater.callout = setTimeout(self.updater.refresh, self.updater.refreshtime);
    }
    self.updater.new_xhReq = function() {
      var xhReq = new XMLHttpRequest();
      xhReq.onreadystatechange = self.updater._progressCallback;
      return xhReq;
    }
    self._toHex = function(str) {
      var r = "";
      var e = str.length;
      var c = 0;
      var h;
      while (c < e) {
        h = str.charCodeAt(c++).toString(16);
        while (h.length<2) 
          h="0"+h;
        r+=h;
      }
      return r;
    }
    self._fromHex = function(str) {
      var r="";
      var e = str.length;
      var s;
      while (e > 0) {
        s = e - 2;
        r = String.fromCharCode("0x"+str.substring(s,e))+r;
        e = s;
      }
      return r;
    }
    self._dbdate_to_jstime = function(dbdate) {
      var datepart;
      var pos = dbdate.lastIndexOf('.');
      if (pos != -1) {
        datepart = dbdate.slice(0, pos-1);
        var mspart = dbdate.slice(pos+1);
      }
      else {
        datepart = dbdate;
      }
      return Date.parse(datepart);
    }
    self._log10 = function(x) {
      return Math.log(x) / Math.log(10);
    }
    self._nextTime = function(time_now) {
      var intdec = time_now.split(".");
      if (intdec.length != 2)
        return time_now;
      return '' + intdec[0] + '.' + (parseInt(intdec[1]) + 1);
    }
    self.updater.requests = new Object();
    self.updater.refreshtime = 10000; // 10 seconds default
    self.updater.refreshcount = 0; // infinite
    self.updater.refresh = function() {
      if (self.updater.xhReq != undefined) {
        self.updater.xhReq.abort();
        delete self.updater.xhReq;
      }
      self.updater.xhReq = self.updater.new_xhReq();
      // Build request string
      var reqstr = "/ajax/get.php?";
      var firstreq = true;
      for (req in self.updater.requests) {
        if (firstreq) {
          reqstr += req;
          firstreq = false;
        }
        else
          reqstr += ( "&" + req );
        var firstarg = true;
        for (arg in self.updater.requests[req]) {
          if (firstarg) {
            reqstr += ( "=" + arg + ":" + self.updater.requests[req][arg] );
            firstarg = false;
          }
          else
            reqstr += ( "," + arg + ":" + self.updater.requests[req][arg] );
        }
      }
      // only send a request if we have anything to get
      if (!firstreq) {
        self.updater.xhReq.open("GET", reqstr);
        self.updater.xhReq.send("");
      }
    }
  }

  self.initdata();
  self.initupdater();
  self.clearrequests();

}
