// Javascript speed check class

function AsyncSpeedCheck()
{
	//
	// Private data
	//
	
	var m_nMinimumAllowedStages = 1;
	var m_nMaximumAllowedStages = 9;
	
	// URL from which to download
	var m_strInfoURL = "";
	// Max time to wait for the speed check to complete
	var m_nMaxTimeout = 0;
	// Max number of stages to use in testing
	var m_nMaxStage = 0;
	// Minimum time for a download to take to be consider an accurate reading 
	// Default: 3 seconds
	var m_nMinTestTime = 3000;
	// Error message
	var m_strLastErrorString = "No error";
	
	// Start time indicator marked before the test begins
	var m_nStartTime = 0;
	// Timeout ID
	var m_nTimeoutID = 0;
	// Current test stage
	var m_nCurrentStage = 0;
	// Most recently calculated speed
	var m_nLastSpeedRating = 0;
	
	
	// XMLHTTP object
	var m_objXmlHttp = null;
	
	// Array of stage objects
	var m_arrStages = null;
	
	
	// Event callbacks to call out to the script in the page
	// which is using the speed check.
	var m_fnOnCompleteCallback = null;
	var m_fnOnNextStageCallback = null;
	
	//
	// Public data and function declaration
	//
	
	this.Start = Start;
	this.SetInfoURL = SetInfoURL;
	this.GetInfoURL = GetInfoURL;
	this.SetMaxTimeout = SetMaxTimeout;
	this.GetMaxTimeout = GetMaxTimeout;
	this.SetMaxStage = SetMaxStage;
	this.GetMaxStage = GetMaxStage;
	this.SetMinTestTime = SetMinTestTime;
	this.GetMinTestTime = GetMinTestTime;
	
	this.GetLastErrorMessage = GetLastErrorMessage;
	this.GetLastSpeedRating = GetLastSpeedRating;
	
	this.SetOnCompleteCallback = SetOnCompleteCallback;
	this.GetOnCompleteCallback = GetOnCompleteCallback;
	this.SetOnNextStageCallback = SetOnNextStageCallback;
	this.GetOnNextStageCallback = GetOnNextStageCallback;
	
	//
	// Private event handlers
	//
	
	// Called when the XmlHttpRequest completes
	function OnGetInfoResult()
	{
		// Process XmlHttp Request results
		if (m_objXmlHttp.readyState == 4)
		{
		
			// if "OK"
			if (m_objXmlHttp.status == 200)
			{
				if(ParseResponse(m_objXmlHttp.responseXML))
				{
					// Use zero length timer to asynchronously start the testing and return to the caller
					setTimeout( PerformTest, 0);
				}
			}
			else
			{
				m_strLastErrorMessage = "Info retrieval failed with an HTTP Error: " + 
					m_objXmlHttp.status + " " + m_objXmlHttp.statusText;
					
				// Call the completion callback
				if(null != m_fnOnCompleteCallback)
				{						
					m_fnOnCompleteCallback(0);
				}	
			}
		}
	
	}
	
	
	
	function OnTestStageResult()
	{
		
		// Grab end time marker and compute elapsed time
		var objEndDate = new Date();
		var nEndTime = objEndDate.getTime();
		
		var nTimeElapsed = nEndTime - m_nStartTime;
		
		// Calculate speed
		var nDataSize = m_arrStages[m_nCurrentStage].size;
		m_nLastSpeedRating = Math.round((nDataSize  * 8 / 1024) / (nTimeElapsed / 1000));
				
		// The speed check may have been successful but the measurement is not considered accurate
		// (Got a burst speed instead of a sustained speed rating)		
		if(nTimeElapsed < m_nMinTestTime)
		{			
			// Continue to next stage if necessary
			if(m_nCurrentStage < m_nMaxStage)
			{
				// Call the "next stage" callback before continuing
				if(null != m_fnOnNextStageCallback)
				{
					m_fnOnNextStageCallback(m_nCurrentStage);
				}
				
				m_nCurrentStage++;
				
				setTimeout( DoTestStage, 0);				
				
			}
			else
			{
				// Clear the timeout if we completed the test
				clearTimeout(m_nTimeoutID);
				
				if("No error" == m_strLastErrorString)
				{
					m_strLastErrorString = "Test never met minimum time requirement.";
				}
				
				Cleanup();
				
				// Call the completion callback
				if(null != m_fnOnCompleteCallback)
				{						
					m_fnOnCompleteCallback(0);
				}
			}
		
		}
		// Got a good rating and the time met the minimum requirements, no need
		// to go to the next stage
		else
		{
			// Clear the timeout if we completed the test
			clearTimeout(m_nTimeoutID);
			
			m_strLastErrorString = "No error";
			
			Cleanup();
					
			// Call the completion callback
			if(null != m_fnOnCompleteCallback)
			{
				
				m_fnOnCompleteCallback(m_nLastSpeedRating);
			}
		}
		
	}
	
	
	function OnTimeout()
	{
		// Kill any download in progress
		m_objXmlHttp.abort();
	
		m_strLastErrorString = "The maximum timeout was exceeded.";
		
		Cleanup();
		
		// Call the completion callback
		if(null != m_fnOnCompleteCallback)
		{
			
			m_fnOnCompleteCallback(0);
		}
	}	
	
	//
	// Operations
	//
	
	function Start()
	{
		// Validate the configuration
		if("" == m_strInfoURL)
		{
			m_strLastErrorString = "The info URL has not been set.";
			return false;
		}
		
		// Max stage must be m_nMinimumAllowedStages to m_nMaximumAllowedStages inclusive
		if(m_nMaxStage < m_nMinimumAllowedStages || m_nMaxStage > m_nMaximumAllowedStages)
		{
			m_strLastErrorString = "An invalid number of maximum test stages was set.";
			return false;
		}
		
		
		// Initialize the xmlhttp object
		if (window.XMLHttpRequest)
		{
			m_objXmlHttp = new XMLHttpRequest();
		}
		else
		{
			try
			{
				m_objXmlHttp = new ActiveXObject("MSXML2.XMLHTTP");
			}
			catch(e)
			{
				try
				{
					m_objXmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
				}
				catch(E)
				{
					m_objXmlHttp = null;
				}
			}
		}
		
		// Make sure we can create an XmlHttp object at all
		if(null == m_objXmlHttp)
		{
			m_strLastErrorString = "The XmlHttp request object could not be created.";
			return false;
		}
		
		// Set the global timeout for the class
		if(0 != m_nMaxTimeout)
		{
			m_nTimeoutID = setTimeout(OnTimeout, m_nMaxTimeout);
		}
		
		
		FetchTestInfo();
		
		return true;		
	}
	
	//
	// Accessors and Manipulators
	//
	
	function SetInfoURL(strNewURL)
	{
		m_strInfoURL = strNewURL;
	}

	function GetInfoURL()
	{
		return m_strInfoURL;
	}
	
	function SetMaxTimeout(nNumberOfMilliseconds)
	{
		m_nMaxTimeout = nNumberOfMilliseconds;
	}
	
	function GetMaxTimeout()
	{
		return m_nMaxTimeout;
	}
	
	function SetMaxStage(nStageNumber)
	{
		m_nMaxStage = nStageNumber;
	}
	
	function GetMaxStage()
	{
		return m_nMaxStage;
	}
	
	function SetMinTestTime(nNumberOfMilliseconds)
	{
		m_nMinTestTime = nNumberOfMilliseconds;
	}
	
	function GetMinTestTime()
	{
		return m_nMinTestTime;	
	}
	
	function GetLastErrorMessage()
	{
		return m_strLastErrorString;	
	}
	
	function GetLastSpeedRating()
	{
		return m_nLastSpeedRating;
	}
	
	// Set the callback function to call when the test is complete
	function SetOnCompleteCallback(fnNewFunction)
	{
		m_fnOnCompleteCallback = fnNewFunction;
	}
	
	// Get the callback function current set to call when the test is complete
	function GetOnCompleteCallback()
	{
		return m_fnOnCompleteCallback;
	}
	
	// Set the callback function to call when the test switches to the next stage
	function SetOnNextStageCallback(fnNewFunction)
	{
		m_fnOnNextStageCallback = fnNewFunction;
	}
	
	// Get the callback function current set to call when the test switches to the next stage
	function GetOnNextStageCallback()
	{
		return m_fnOnNextStageCallback;
	}
	
	//
	// Internal functions
	//
	
	function FetchTestInfo()
	{
		var strFetchURL = m_strInfoURL;
		var nRandomNumber = (Math.random() * Number.MAX_VALUE);
		
		// Construct the URL to be used
		if(-1 == strFetchURL.indexOf("?"))
		{
			strFetchURL += "?MaxLevels=" + m_nMaxStage;
		}
		else
		{
			strFetchURL += "&MaxLevels=" + m_nMaxStage;
		}
		
		strFetchURL += "&value=" + nRandomNumber;
		
		// Set up XmlHttp object properties
		m_objXmlHttp.open("GET",strFetchURL,true);			
		m_objXmlHttp.onreadystatechange = OnGetInfoResult;
		
		// Non-ActiveX browsers, slightly different "send" call
		if(window.XMLHttpRequest)
		{
			m_objXmlHttp.send(null);
		}
		// IE
		else
		{
			m_objXmlHttp.send();
		}
	
	}
	
	function ParseResponse(objXMLResponse)
	{
		// Get the list of all "Level" nodes provided
		var objLevels = null;
		for(var n = 0; n < objXMLResponse.childNodes.length; n++)
		{
			if(objXMLResponse.childNodes[n].nodeName == "Levels")
			{
				objLevels = objXMLResponse.childNodes[n].childNodes;
			}
		}
		
		if(null == objLevels)
		{
			m_strLastErrorMessage = "Unrecognized XML structure in speed check info."
			return false;
		}
			
		m_arrStages = new Array(objLevels.length);
		
		// Iterate through each "Level" node and create a "Stage" object for it.
		for(var i = 0; i < objLevels.length; i++)
		{
			var objStageObject = new Object();
						
			// Look through the property names rather than using index
			// Easier to understand the code, and rearranging or adding properties
			// to the node won't break this.  Think of it as a barebones implementation
			// of XMLNode.selectNodes()
			var objProperties = objLevels[i].childNodes;
			
			for(var j = 0; j < objProperties.length; j++)
			{	
				// Set the appropriate Stage object property from the XML property node
				switch((objProperties[j].nodeName).toUpperCase())
				{
					case "ID":
						objStageObject.id = Number(objProperties[j].firstChild.nodeValue) + 1;
						break;
				
					case "URL":
						objStageObject.URL = objProperties[j].firstChild.nodeValue;
						break;
						
					case "SIZE":
						objStageObject.size = Number(objProperties[j].firstChild.nodeValue);
						break;
					
					default:
						break;
				}
			}
			
			// Add the stage object to the array
			m_arrStages[i] = objStageObject;
		}
			
		return true;
	}
	
	function PerformTest()
	{
		// Create a hidden image tag to load for stages
		var objNewImageTag = document.createElement("img");
		objNewImageTag.setAttribute("id", "SpeedCheck_StageImage");
		objNewImageTag.setAttribute("name", "SpeedCheck_StageImage");
		objNewImageTag.style.display = "none";
		document.body.appendChild(objNewImageTag);
	
		// Start the testing at stage 1
		m_nCurrentStage = 1;
		DoTestStage();
	}
	
	function DoTestStage()
	{
		var strNoCacheURL = m_arrStages[m_nCurrentStage - 1].URL;
		var nRandomNumber = (Math.random() * Number.MAX_VALUE);
		
		// Construct the URL to be used
		if(-1 == strNoCacheURL.indexOf("?"))
		{
			strNoCacheURL += "?value=" + nRandomNumber;
		}
		else
		{
			strNoCacheURL += "&value=" + nRandomNumber;
		}
		
		// Grab the test time marker
		var objStartDate = new Date();
		m_nStartTime = objStartDate.getTime();
		
		// Set the source of the image tag to begin the download		
		var objImg = document.images["SpeedCheck_StageImage"];
		if(null == objImg || undefined == objImg)
		{
			m_strLastErrorString = "Could not find the image tag in the document.";
			Cleanup();
			// Call the completion callback
			if(null != m_fnOnCompleteCallback)
			{				
				m_fnOnCompleteCallback(0);
			}
			
		}
		else
		{
			// Set the callback and load the image
			objImg.onload = OnTestStageResult;
			objImg.setAttribute("src", strNoCacheURL);
		}
	
	}
	
	function Cleanup()
	{
		// Remove the image tag when we no longer need it.
		document.body.removeChild(document.getElementById("SpeedCheck_StageImage"));
	}
	
}

