/* *** Config *** */

var MaxPasses       = 10;     //
var MaxSizeTime     = 0.5;    // Seconds
var TargetLoadTime  = 1;      // Seconds
var MaxFileSize     = 500e3;  // Bytes
var FilterDrop      = 0.4;    // percent
var DownFudge       = 1.010;  // multiplier
var UpFudge         = 1.000;  // multiplier



/* *** Globals *** */

var TimeTechnique;   // see getMilliSec()
var PassNr;          //
var RunTime;         // Seconds
var FileSize;        // Bytes
var LatencyTime;     // Seconds
var LoadTime;        // Seconds
var State = 0;       // see RunTest()
var divBar;          //


/* *** Code *** */

// Used by the calling program to set up the div's we need so they are in the right place.

function InitTest() {
  div1 = makeDiv();
  div2 = makeDiv();
  div3 = makeDiv();
  div4 = makeDiv();
  div5 = makeDiv();
  div6 = makeDiv();
  div7 = makeDiv();
}


function SpeedTest() {
  // Opening text & Start button.
  putInDiv(div1, 
    '<p><br><p>' + 
    'This test measures the speed between our server and your computer.' + 
    '<p>' + 
    'For best results, stop any other programs which use the Internet while you are running this test.' + 
    '<p>' + 
    'If you are using a Dial-In 56K or ISDN modem then disable compression while running this test.' + 
    '<p><br><p>', 
    "center"
  );
  putInDiv(div2, 
    '<p><br><p>' + 
    '<button name="startButton" onclick="RunTest();" tabindex="1"><b style="color: maroon">Start</b></button>' + 
    '<p>', 
    "center"
  );
  startButton.focus();
}


// This is the mainline code.  Due to the data transfers using exceptions, 
// it is implemented as a state machine using "State".

function RunTest() {
  State++;
  switch(State) {

      // Init some stuff.
    case 1:
      putInDiv(div1, '');
      putInDiv(div2, '');
      putInDiv(div3, '');
      putInDiv(div4, '');
      putInDiv(div5, '');
      putInDiv(div6, '');
      putInDiv(div7, '');
      getMilliSecSetup();

      State++;  // Fall through to next case

      // Down size test.
    case 2:
      RunTime = 0;  // Flag the initial call
      doDownSize(RunTest);
      break;

      // Down load test.
    case 3:
      RunTime = 0;  // Flag the initial call
      doDownLoad(RunTest);
      break;

      // Up size test.
    case 4:
      RunTime = 0;  // Flag the initial call
      doUpSize(RunTest);
      break;

      // Up load test.
    case 5:
      RunTime = 0;  // Flag the initial call
      doUpLoad(RunTest);
      break;

      // Finish up.
    case 6:
      State = 0;
      // Repeat button.
      putInDiv(div7, 
        '<p><br><p>' + 
        '<button name="repeatButton" onclick="RunTest();" tabindex="1"><b style="color: maroon">Repeat</b></button>' + 
        '<p><br><p>',  
        "center"
      );
      addToDiv(div7, '<embed src="http://www.sstar.com/speedtest/images/road_run.wav" autostart="true" hidden="true">');
      repeatButton.focus();
      break;

    default:
      alert("Illegal state = " + State);
      break;
  }
}


// Download file sizing.

function doDownSize (callback) {
  var time;

  if (RunTime == 0) {
    // Handle initial call.
    LatencyTime = new Array();
    LoadTime = new Array();
    FileSize = 1024;
    showSize(div3, 'Down', FileSize);
    pull("null_dn.php?len=" + FileSize, "", function() { doDownSize(callback); });
  } else if (LatencyTime.length == LoadTime.length) {
    // Handle latency results.
    LatencyTime.push(RunTime);
    pull("data_dn.php?len=" + FileSize, "", function() { doDownSize(callback); });
  } else {
    // Handle load results.
    LoadTime.push(RunTime);
    time = LoadTime[LoadTime.length - 1] - LatencyTime[LatencyTime.length -1];
    time = Math.max(time, 1e-12);
    if (((time >= (MaxSizeTime * 0.6)) && (time < (MaxSizeTime * 1.5))) ||
        (FileSize >= MaxFileSize)) {
      // Done.  Finish up.
      FileSize *= TargetLoadTime / time; 
      FileSize = Math.min(FileSize, MaxFileSize);
      showSize(div3, 'Down', FileSize);
      callback();
    } else {
      // Bump the size and try again.
      if (time >= (MaxSizeTime * 1.5)) {
        FileSize *= 0.75;
      } else {
        FileSize = Math.min(FileSize * 2, MaxFileSize);
      }
      showSize(div3, 'Down', FileSize);
      pull("null_dn.php?len=" + FileSize, "", function() { doDownSize(callback); });
    }
  }
}


// Upload file sizing.

function doUpSize (callback) {
  var time;
  var data;

  if (RunTime == 0) {
    // Handle initial call.
    LatencyTime = new Array();
    LoadTime = new Array();
    FileSize = 1024;
    showSize(div5, 'Up', FileSize);
    data = "a";
    pull("data_up.php", "data=" + data, function() { doUpSize(callback); });
  } else if (LatencyTime.length == LoadTime.length) {
    // Handle latency results.
    LatencyTime.push(RunTime);
    data = genStr(FileSize);
    pull("data_up.php", "data=" + data, function() { doUpSize(callback); });
  } else {
    // Handle load results.
    LoadTime.push(RunTime);
    time = LoadTime[LoadTime.length - 1] - LatencyTime[LatencyTime.length -1];
    time = Math.max(time, 1e-12);
    if (((time >= (MaxSizeTime * 0.6)) && (time < (MaxSizeTime * 1.5))) ||
        (FileSize >= MaxFileSize)) {
      // Done.  Finish up.
      FileSize *= TargetLoadTime / time; 
      FileSize = Math.min(FileSize, MaxFileSize);
      showSize(div5, 'Up', FileSize);
      callback();
    } else {
      // Bump the size and try again.
      if (time >= (MaxSizeTime * 1.5)) {
        FileSize *= 0.75;
      } else {
        FileSize = Math.min(FileSize * 2, MaxFileSize);
      }
      showSize(div5, 'Up', FileSize);
      data = "a";
      pull("data_up.php", "data=" + data, function() { doUpSize(callback); });
    }
  }
}


// Download speed test.

function doDownLoad (callback) {
  var time;
  var speed;

  if (RunTime == 0) {
    // Handle initial call.
    PassNr = 1;
    LatencyTime = new Array();
    LoadTime = new Array();
    divBar = makeProg(div4, "left");
    showProg (divBar, 0);
    pull("null_dn.php?len=" + FileSize, "", function() { doDownLoad(callback); });
  } else if (LatencyTime.length == LoadTime.length) {
    // Handle latency results.
    LatencyTime.push(RunTime);
    pull("data_dn.php?len=" + FileSize, "", function() { doDownLoad(callback); });
  } else {
    // Handle load results.
    LoadTime.push(RunTime);
    time = Math.max((LoadTime.filter() - LatencyTime.filter()), 1e-12);
    speed = (FileSize * 8) / time;
    showSpeed(div3, 'Down', speed * DownFudge);
    showProg (divBar, PassNr / MaxPasses);
    if (PassNr++ == MaxPasses) {
      // Done.
      showProg (divBar, 0);
      removeBox(div4);
      callback();
    } else {
      // Go again.
      pull("null_dn.php?len=" + FileSize, "", function() { doDownLoad(callback); });
    }
  }
}


// Upload speed test.

function doUpLoad (callback) {
  var time;
  var speed;
  var data;

  if (RunTime == 0) {
    // Handle initial call.
    PassNr = 1;
    LatencyTime = new Array();
    LoadTime = new Array();
    divBar = makeProg(div6, "right");
    showProg (divBar, 0);
    data = "a";
    pull("data_up.php", "data=" + data, function() { doUpLoad(callback); });
  } else if (LatencyTime.length == LoadTime.length) {
    // Handle latency results.
    LatencyTime.push(RunTime);
    data = genStr(FileSize);
    pull("data_up.php", "data=" + data, function() { doUpLoad(callback); });
  } else {
    // Handle load results.
    LoadTime.push(RunTime);
    time = Math.max((LoadTime.filter() - LatencyTime.filter()), 1e-12);
    speed = (FileSize * 8) / time;
    showSpeed(div5, 'Up', speed * UpFudge);
    showProg (divBar, PassNr / MaxPasses);
    if (PassNr++ == MaxPasses) {
      // Done.
      showProg (divBar, 0);
      removeBox(div6);
      callback();
    } else {
      // Go again.
    data = "a";
    pull("data_up.php", "data=" + data, function() { doUpLoad(callback); });
    }
  }
}



/* *** Utility Functions *** */

// Generate the data string to send.

function genStr(len) {
  var ret = 'aaaaaaaaaaaaaaa ';
  while (1) {
    if (ret.length > len) {
      return ret.substr(0, len);
    }
    if (ret.length * 2 >= len) {
      return ret + ret.substr(0, len - ret.length);
    }
    ret += ret;
  }
}


// The next 2 functions are used to move the data back and forth.

function getHttpRequester() {
  var xmlhttp=false;
  /*@cc_on @*/
  /*@if (@_jscript_version >= 5)
   try {
    xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
   } catch (e) {
     try {
       xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
     } catch (E) {
       xmlhttp = false;
    }
   }
  @end @*/
  if (!xmlhttp && typeof XMLHttpRequest!='undefined') {
    xmlhttp = new XMLHttpRequest();
  }
  return xmlhttp;
}


// This moves the data.
// Note:  I don't use "opt_method" but left it in anyway.

function pull(src, post, callback, opt_method) {
   if (src) {
      var X = getHttpRequester();
      if (X) {
        X.open("POST", src, true);
        X.onreadystatechange = function() {
          if (X.readyState == 4) {
            var end = getMilliSec();
            RunTime = Math.max((end - start) / 1e3, 1e-12);  // Avoid returning 0
            if (opt_method != null) {
              if (!callback[opt_method]) {
                alert(opt_method + " was not found within class");
              }
              callback[opt_method]();
            } else if (callback) {
              callback();
            } 
          } 
        };
        X.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        var start = getMilliSec();
        X.send(post);
      } else {
        alert("pull called but the browser doesn't seem to support the XMLHttpRequest object.");
      }
    } else {
      alert("pull called with an invalid source = null");
    }
}


// Used to sort arrays in numeric, not alpha, order.

function sortNumber(x, y) {
  return x - y
}


// Filter out the largest values in an array.  Return the average of the 
// remaining elements.

Array.prototype.filter = function() {
  this.sort(sortNumber);
  var drop = Math.round(FilterDrop * this.length);
  var bottom = 0;
  var top = this.length - drop - 1;
  var sum;
  for (var i = bottom, sum = 0; i <= top; sum += this[i++]);
  return sum / (top - bottom + 1);
}


// Determine which technique we should use for getMilliSec().
// Set TimeTechnique accordingly.  The smaller the resolution, the 
// better.  The JS function always works but the two Java 
// techniques might or might depending on the browser and machine.
// And, which of the two varies also.
//   1 = JavaScript's Date() function
//   2 = Java's java.lang.System.nanoTime() method
//   3 = Java's java.lang.System.nanoTime() method via an applet

function getMilliSecSetup() {
  var best, t1, t2;

  // Try technique #1.
  t1 = new Date();
  t1 = t1.getTime();
  do {
    t2 = new Date();
    t2 = t2.getTime();
  } while (t1 == t2)
  best = t2 - t1;
  TimeTechnique = 1;

  if (navigator.javaEnabled()) {

    // Try technique #2.
    try {
      t1 = java.lang.System.nanoTime();
      do {
        t2 = java.lang.System.nanoTime();
      } while (t1 == t2)
      if ( ! (isNaN(t1) || isNaN(t2) || (t2 - t1 == 0)) ) {
        if ((t2 - t1) < best) {
          best = t2 - t1;
          TimeTechnique = 2;
        }
      }
    } catch (e) {
      ;
    }

    // Try technique #3.
    try {
      t1 = document.getmilliapplet.getNanoTime();
      do {
        t2 = document.getmilliapplet.getNanoTime();
      } while (t1 == t2)
      if ( ! (isNaN(t1) || isNaN(t2) || (t2 - t1 == 0)) ) {
        if ((t2 - t1) < best) {
          best = t2 - t1;
          TimeTechnique = 3;
        }
      }
    } catch (e) {
      ;
    }

  }

}


// Use the chosen technique to return the time in milliseconds.
//   1 = JavaScript's Date() function
//   2 = Java's java.lang.System.nanoTime() method
//   3 = Java's java.lang.System.nanoTime() method via an applet

function getMilliSec() {
  switch (TimeTechnique) {
    case 1: { 
      var x = new Date();
      return x.getTime();
    }
    case 2: return java.lang.System.nanoTime() / 1e6;
    case 3: return document.getmilliapplet.getNanoTime() / 1e6;
  }
}


// The next group of functions are used to manage the div's on the screen.

function makeDiv () {
  var div = document.createElement("div");
  document.body.appendChild(div);
  return div;
}


function putInDiv (div, str, align, weight, color) {
  if (align != null) {
    div.style.textAlign = align;
    if (weight != null) {
      div.style.fontWeight = weight;
      if (color != null) {
        div.style.color = color;
      }
    }
  }
  div.style.fontSize = "larger";
  div.innerHTML = str;
}


function addToDiv (div, str) {
  div.innerHTML += str;
}


// Note:  This function isn't actually used, but I left it in for reference.

function removeDiv (div) {
  div.parentNode.removeChild(div);
}


// Make a progress box with a bar in it, inside a div.

function makeProg (divBox, align) {

  divBox.style.width = "200px";
  divBox.style.height = "16px";
  divBox.style.padding = "1px";
  divBox.style.border = "1px solid blue";
  divBox.style.marginLeft = "auto";
  divBox.style.marginRight = "auto";
  divBox.style.textAlign = align;  // @@ Only seems to work with IE

  var divBar = makeDiv();
  divBar.style.width = "0px";
  divBar.style.height = "16px";
  divBar.style.backgroundColor = "red";
  divBar.style.fontSize = "0px";  // IE workaround
  divBox.appendChild(divBar);

  return divBar;
}


// Display the progress.

function showProg (div, percent) {
  div.style.width = (percent * 100) + "%";
}


// Remove a box from a div.
// @@ A more generic initDiv() would me nice.

function removeBox (divBox) {
  divBox.style.width = "100%";
  divBox.style.height = "100%";
  divBox.style.padding = "0px";
  divBox.style.border = "0px solid blue";
}


function showSize (div, direction, size) {
  size = (size / 1024).toFixed(0);
  putInDiv(div,
    '<big>' + 
    direction + 'load Size:&nbsp; ' + size + " KB" +
    '</big>' +
    '<p>', 
    'center', 
    'lighter', 
    'blue'
  );
}


// Display the speed.

function showSpeed (div, direction, speed) {
  var speedStr
  if (speed < 10e6) {
    speedStr = (speed / 1e3).toFixed(0) + " Kbps";
  } else {
    speedStr = (speed / 1e6).toFixed(1) + " Mbps";
  }
  putInDiv(div,
    '<big>' + 
    direction + 'load Speed:&nbsp; ' + speedStr +
    '</big>' +
    '<p>', 
    'center', 
    'bold', 
    'blue'
  );
}


/* *** End *** */
