/* speechy.js

   Copyright (c) 2006 Alex S. Brown, All rights reserved.
   
   Designed to be used with slidy.js from W3C. For more information see
   http://www.w3.org/Talks/Tools/Slidy
*/

// Array of the start times of the media for each slide
var slideStartTimes;
// Holds the previous value for the slide number and increment, based on the media player position
// Set both to -1 to start, to ensure that the player position will be set
var prevSlideInc = new Array(-1,-1);

// Set a different onload event, to do the typical slidy
// functions, plus the player set up
window.onload = startupSpeechy; // equivalent to onload on body element


function startupSpeechy()
{
	// Gather the slide end times array
	slideStartTimes = findSlideStartTimes();

	// Run the slidy startup function
	startup();

	// Slidy assumes that incremental items should be shown by default,
	// but we want them hidden by default.
	lastShown = null;
	setVisibilityAllIncremental("hidden");
	
	// Create the player object
	addPlayerObject();
	// Do not try to set the position right away, because the media player
	// might not yet be loaded. It seems to cause problems with Windows Media Player
	// Check the slide position every half-second
	var timerID = setInterval("checkSync()", 500);
}

// Get the media file URL from the meta tag
// in the header.
function findSpeechyMediaURL()
{
   var name, content;
   var meta = document.getElementsByTagName("meta");

   for (var i = 0; i < meta.length; ++i)
   {
      name = meta[i].getAttribute("name");
      content = meta[i].getAttribute("content");

      if (name == "speechyMediaURL")
         return content;
   }

   return null;
}

// Get the media mime type from the meta tag
// in the header.
function findSpeechyMediaMimeType()
{
   var name, content;
   var meta = document.getElementsByTagName("meta");

   for (var i = 0; i < meta.length; ++i)
   {
      name = meta[i].getAttribute("name");
      content = meta[i].getAttribute("content");

      if (name == "speechyMediaMimeType")
         return content;
   }

   return null;
}

// Get the player width from the meta tag
// in the header.
function findSpeechyPlayerWidth()
{
   var name, content;
   var meta = document.getElementsByTagName("meta");

   for (var i = 0; i < meta.length; ++i)
   {
      name = meta[i].getAttribute("name");
      content = meta[i].getAttribute("content");

      if (name == "speechyPlayerWidth")
         return content;
   }

   return null;
}

// Get the player height from the meta tag
// in the header.
function findSpeechyPlayerHeight()
{
   var name, content;
   var meta = document.getElementsByTagName("meta");

   for (var i = 0; i < meta.length; ++i)
   {
      name = meta[i].getAttribute("name");
      content = meta[i].getAttribute("content");

      if (name == "speechyPlayerHeight")
         return content;
   }

   return null;
}

// Get the start times for each slide
// Returned as an array of arrays
// Each element in the outer array represents a slide.
// The array in each element contains the time for the slide followed by the times
// for each incremental element on the slide.
function findSlideStartTimes()
{
	var name, content;
	var meta = document.getElementsByTagName("meta");
	var result;
	
	for (var i = 0; i < meta.length; ++i)
	{
		name = meta[i].getAttribute("name");
		content = meta[i].getAttribute("content");
		
		// Split the slides based on the semicolons
		if (name == "slideStartTimes")
			result = content.split(";");
	}
	
	// Split each slide's record based on dashes
	for (i = 0; i < result.length; ++i)
	{
    result[i] = result[i].split("-");
  }

	return result;
}

// remove all children from a node element
function removeChildrenFromNode(node)
	{
	if(node == undefined ||
		node == null)
	{
		return;
	}   
	while (node.hasChildNodes())
	{
		node.removeChild(node.firstChild);
	}
}

// clone all children from a node element into a document fragment
function cloneChildrenFromNode(node)
{
	if(node == undefined ||
		node == null)
	{
		return;
	}
	var resultNode = document.createDocumentFragment();
	
	var nodeChildren = node.childNodes;
	
	var nodeChild;
	
	for( var i = 0; i < nodeChildren.length; i++ )
	{
		nodeChild = nodeChildren.item(i);
		resultNode.appendChild(nodeChild.cloneNode(true));
	}
	return resultNode;
}

// Build the tags for the player
// Build different tags depending on the browser capabilities.
// If a new browser or media player becomes available, add
// code to detect and create the tags for the new player.
// By design, this does not inlcude the most basic browser support.
function createPlayerNode(mediaURL, mediaMimeType, playerWidth, playerHeight)
{
	// Create a variable to hold the result node
	var resultNode;
	
	// First create the browser-specific tags
		
	// Fill out the object attributes and parameters for any
	// ActiveX-enabled browser on a Windows platform
	// Might be best to STOP checking for Windows, if
	// other platforms share the same Class ID for Windows Media Player.
	if (window.ActiveXObject && navigator.userAgent.indexOf('Windows') >= 0) {
		// The class id value for Windows Media Player 6.4.
		// No need for a more recent version, but change the value to
		// use another version if you need different capabilities.
		var wmpClassId = "CLSID:22D6f312-B0F6-11D0-94AB-0080C74C7E95";
		// Yes, this is a silly way to create an object tag, but IE has limitations
		// that would require that the classid be set after the item is already inserted
		// in the document for the object tag to work otherwise.
		resultNode = document.createElement('<object classid="'+wmpClassId+'"></object>');
		
		// Create a parameter with the file name of the sound
		var fileNameParam = document.createElement("param");
		fileNameParam.setAttribute("name", "FileName");
		fileNameParam.setAttribute("value", mediaURL);
		
		// Add the parameter as a child to the object tag
		resultNode.appendChild(fileNameParam);
		
	// Otherwise, return the standards-based object tag.
	}else{
		resultNode = document.createElement("object");
		resultNode.setAttribute("type", mediaMimeType);
		resultNode.setAttribute("data", mediaURL);
	}
	
	// Set the attributes and values for all browsers

	// Set the ID value
	resultNode.setAttribute("id", "speechyPlayer");

	// Set the width and height for the player
	if (playerWidth != "")
		resultNode.setAttribute("width", playerWidth);
	if (playerHeight != "")
		resultNode.setAttribute("height", playerHeight);

	return resultNode;
}

// Create player object to play the media
// It finds an element named 'speechyPlayerArea', creates an
// object tag with a compatible player within it,
// then inserts the old contents of the element as
// alternate content if the object tag is not supported.
function addPlayerObject()
{
	// Get the node and media information
	var playerNode = document.getElementById("speechyPlayerArea");
	var mediaURL = findSpeechyMediaURL();
	var mediaMimeType = findSpeechyMediaMimeType();
	
	// Create a document fragment with the contents of playerNode
	var fallbackCode = cloneChildrenFromNode(playerNode);
	
	var playerObjectNode = createPlayerNode(mediaURL, mediaMimeType, findSpeechyPlayerWidth(), findSpeechyPlayerHeight());
	
	removeChildrenFromNode(playerNode);
	playerNode.appendChild(playerObjectNode);

	// Add fallback code within the object tag
	// This code seems to be causing errors in IE, but not sure why
//	playerObjectNode.appendChild(fallbackCode);
}

// Set the position in the player object.
// Add more player- and browser-specific methods and properties
// as they are implemented.
// Have seen problems with VLC and Ogg because it cannot determine the length
// of Ogg files, so it cannot seek by time. It may be able to seek by percentage.
// Should try having the length of the audio in a meta tag and setting position
// by percentage instead of time.
function setPlayerPosition(secs)
{
	var speechyPlayerObject = document.getElementById("speechyPlayer");

	// Try a QuickTime method based on its own timescale
	try
	{
		speechyPlayerObject.SetTime(secs * speechyPlayerObject.GetTimeScale());
	} catch(e) {
		// Try a RealPalyer method based on milliseconds
		try
		{
			var millisecs = Math.round(secs * 1000);
			speechyPlayerObject.SetPosition(millisecs);
		} catch(e) {
			// Try a VLC method
			// Had to disable
			//try
			//{
				// Seek goes to exact position if second argument is not true
				// Disabled because testing showed seek to always reset back to the start
				// under Windows XP Firefox 1.5 VLC 0.8.4a
				// speechyPlayerObject.seek(secs, null);
			//} catch(e) {
			
			// Try a Netscape LiveAudio method in a try-catch block
			try
			{
				speechyPlayerObject.start_time(secs);
			} catch(e) {
				// Try setting all properties last, because setting a brand
				// new property will not raise an error in some browsers
				
				// Set property for Windows Media Player version 7 and above
				try
				{
					speechyPlayerObject.currentPosition = secs;
					// Set property for WMP v6.4, too
					speechyPlayerObject.selectionStart = secs;
					// Set property for VLC version 0.8.5.1 and greater
					speechyPlayerObject.input.time = secs * 1000;
				} catch(e) {
					// Set property for WMP version 6.4 and VLC again
					// Do it again, because setting the property above might have
					// raised an error
					try
					{
					speechyPlayerObject.selectionStart = secs;
					speechyPlayerObject.input.time = secs * 1000;
					} catch(e) {
						// Set VLC property for version 0.8.5.1 and greater again
						try
						{
							// Set the input time in milliseconds
							speechyPlayerObject.input.time = secs * 1000;
						} catch(e) {
							// Nothing worked
							return null
						}
					}
				}
			}
		}
	}
}


// Get the position in seconds
function getPlayerPosition()
{
	var speechyPlayerObject = document.getElementById("speechyPlayer");
	
	var result;
	// Try a QuickTime method, returns time in its own time scale
	try
	{
		result = speechyPlayerObject.GetTime() / speechyPlayerObject.GetTimeScale();
	} catch(e) {
		// Try a RealPlayer method, returns time in milliseconds
		try
		{
			result = speechyPlayerObject.GetPosition() / 1000;
		} catch(e) {
			// Try a VLC method, returns time in seconds
			// Should work only for versions on or before 0.8.5
			try
			{
				result = speechyPlayerObject.get_time();
			} catch(e) {
				// Try a VLC method, returns time in milliseconds
				// Should work only for versions after 0.8.5
				try
				{
					result = speechyPlayerObject.input.time / 1000;
				} catch(e) {
					// Get property to get the position, from Windows Media Player.
					var playerPosition = speechyPlayerObject.currentPosition;
					// Convert strings to numbers, if needed
					// WMP returns whole numbers as strings sometimes
					if ( typeof(playerPosition) == 'string')
						playerPosition = Number(playerPosition);
					if ( typeof(playerPosition) == 'number' )
						result = playerPosition;
					else
						result = null;
				}
			}
		}
	}
	return result;
}

// Function to determine if the player is ready to play.
// Returns false if still loading media or otherwise busy.
// Did  not test all states of all players, so made assumptions based on
// documentation available.
function isPlayerReady()
{
	var speechyPlayerObject = document.getElementById("speechyPlayer");
	
	var result;
	// Try the QuickTime method
	try
	{
		switch(speechyPlayerObject.GetPluginStatus())
		{
		case "Waiting":
			result = false
			break
		case "Loading":
			result = false
			break
		case "Playable":
			result = true
			break
		case "Complete":
			result = true
			break
		default:
			result = null
		};
	} catch(e) {
		// Try a RealPalyer method
		try
		{
			switch(speechyPlayerObject.GetPlayerState())
			{
			// Player currently stopped
			case 0:
				result = false
				break
			// Currently contacting
			case 1:
				result = false
				break
			// Currently buffering
			case 2:
				result = false
				break
			// Currently playing
			case 3:
				result = true
				break
			// Currently paused
			case 4:
				result = true
				break
			// Currently seeking
			case 5:
				result = false
				break
			// Currently displaying a modal dialog box
			case 6:
				result = false
				break
			default:
				result = null
			};
		} catch(e) {
			// Try a VLC method
			// There is no useful method for this test before v0.8.5.1
			// but this method should work for v0.8.5.1 and after.
			try
			{
				switch(speechyPlayerObject.input.state)
				{
				// Player currently idle or closed
				case 0:
					result = false
					break
				// Currently opening
				case 1:
					result = false
					break
				// Currently buffering
				case 2:
					result = false
					break
				// Currently playing
				case 3:
					result = true
					break
				// Currently paused
				case 4:
					result = true
					break
				// Currently stopping
				case 2:
					result = true
					break
				// Currently in error
				case 2:
					result = false
					break
				default:
					result = null
				};
			} catch(e) {
				// Try a Windows Media Player method
				// Should work for version 7.0 and greater
				switch(speechyPlayerObject.playState)
				{
				// Player state undefined
				case 0:
					result = false
					break
				// Stopped
				case 1:
					result = true
					break
				// Paused
				case 2:
					result = true
					break
				// Playing
				case 3:
					result = true
					break
				// Fast forwarding
				case 4:
					result = false
					break
				// Rewinding
				case 5:
					result = false
					break
				// Buffering
				case 6:
					result = false
					break
				// Waiting -- no data yet
				case 7:
					result = false
					break
				// Media ended playback
				case 8:
					result = true
					break
				// Transitioning, preping new media
				case 9:
					result = false
					break
				// Ready to begin playing
				case 10:
					result = true
					break
				// Reconnecting to the stream
				case 11:
					result = false
					break
				}
			}
		}
	}
	return result;
}


// New function based on slidy methods, to jump to a particular slide
// Pass 0 for first slide, 1 for second, etc.
function gotoSlide(gotoslidenum)
{
   if (!viewAll)
   {
      var slide;

      if (gotoslidenum < slides.length)
      {
         slide = slides[slidenum];
         hideSlide(slide);

         slidenum = gotoslidenum;
         slide = slides[slidenum];
         lastShown = null;
         setVisibilityAllIncremental("hidden");
         showSlide(slide);
      }

      setLocation();

      if (!ns_pos)
         refreshToolbar(200);
   }
}

// New function to jump to a particular slide and increment number
// Pass 0 for first slide, 1 for second, etc.
// Pass 0 for no incremental items shown, 1 for first shown, etc.
function gotoSlideInc(gotoslidenum, gotoincnum)
{
   if (!viewAll)
   {
      var slide;

      if (gotoslidenum < slides.length)
      {
         slide = slides[slidenum];
         hideSlide(slide);

         slidenum = gotoslidenum;
         slide = slides[slidenum];
         // Need to make modifications here to show the incremental items
         lastShown = null;
         setVisibilityAllIncremental("hidden");
         for (var i = 0; i < gotoincnum; ++i)
         {
           lastShown = revealNextItem(lastShown);
         }
         showSlide(slide);
      }

      setLocation();

      if (!ns_pos)
         refreshToolbar(200);
   }

}

// Get the target slide and increment number from a position in seconds
function getSlideIncNumberFromSecs(posSecs)
{
	var result, slideresult, incresult;
	var slideinctimes;
	if (posSecs == null)
	{
		result = null;
	}
	else
	{
		// Loop through all the start times for the slides
		// looking for the slide number that best matches the position
		for (var i = 0; i < slideStartTimes.length; ++i)
		{
      slideinctimes = slideStartTimes[i];
      // if the position is greater than the start time, then
			// the slide is at least the index
			if (posSecs >= slideinctimes[0])
			{
				slideresult = i;
			}
		}
		// Loop through all the start times for the increments in the slide result value
		// to find the increment within the slide that best matches the position
    if (slideresult <= slideStartTimes.length)
    {
      slideinctimes = slideStartTimes[slideresult];
      for (var i = 0; i < slideinctimes.length; ++i)
      {
        if (posSecs >= slideinctimes[i])
        {
          incresult = i;
        }
      }
    }
    result = new Array(2);
    result[0] = slideresult;
    result[1] = incresult;
	}
	return result;
}

// Get the current slide and increment number
// It is trivial to get the slide number, but the increment
// number is not stored anywhere in Slidy, so it takes some extra
// work.
// Returns an array with two elements, first the slide number,
// then the increment number.
function getCurrentSlideInc()
{
  var result;
  var node = nextIncrementalItem(null);
  var incnumber = 0;

  while (node && node.style.visibility == "visible")
  {
    incnumber = incnumber + 1;
    node = nextIncrementalItem(node);
  }
  result = new Array(2);
  result[0] = slidenum;
  result[1] = incnumber;
  return result;
}

// Get the starting time for a given slide number
function getSlideIncStartPos(myslideincnumber)
{
	// Set the result to null by default
	var result = null;
	// Decompose the array of slide and increment numbers
	var myslidenumber = myslideincnumber[0];
	var myincnumber = myslideincnumber[1];
	
	// If the slide number is out of bounds, reset it
	if (myslidenumber < 0) myslidenumber = 0;
	if (myslidenumber >= slideStartTimes.length) myslidenumber = slideStartTimes.length - 1;
	
	// Get the array of starts time for the slide
	var mySlideTimeArray = slideStartTimes[myslidenumber];
	
	// If the increment number is out of bounds, reset it
	if (myincnumber < 0) myincnumber = 0;
	if (myincnumber >= mySlideTimeArray.length) myincnumber = mySlideTimeArray.length - 1;
	
	// Return the start time for the increment
	result = mySlideTimeArray[myincnumber];
	return result;
}

// IE sets the position back to zero when it gets to the
// end of the media or if it is stopped. This function
// detects that it is stopped, so we do not automatically
// jump to the beginning.
function isWMPStopped()
{
	var speechyPlayerObject = document.getElementById("speechyPlayer");

	var result = false;
	// Try to check the state of the player using Media Player property
	try
	{
    // The playState property has two different values that mean "stopped"
    // depending on the WMP version. For our purposes, either is good enough.
    // Be aware that the player might actually be paused or stopped.
    var curPlayState = speechyPlayerObject.playState;
    if (curPlayState == 1 || curPlayState == 0 )
      result = true;
	} catch(e) {
    return result;
 }
 
 return result;

}

// Advance to the next slide if the player has moved on past it
// Only advance if the player crossed from the currently displayed
// slide to the next.
function syncSlideToPlayer()
{
	// Get the current position in the player and find out
	// which slide we should be displaying.
	var curPos = getPlayerPosition();
	var targetSlideInc = getSlideIncNumberFromSecs(curPos);
	var currentSlideInc = getCurrentSlideInc();
  // Detect whether we are playing using WMP and the player is stopped
	var isStopped = isWMPStopped();
	// If we are not displaying that slide and WMP is not stopped, switch to the slide
  if ( targetSlideInc != null && isStopped != true )
  {
    // If either the current and target slide or incremental number is different, go to the target
    if ( targetSlideInc[0] != currentSlideInc[0] || targetSlideInc[1] != currentSlideInc[1] )
		  gotoSlideInc(targetSlideInc[0], targetSlideInc[1]);
  }
}

// Set the player position to match the current slide
function syncPlayerToSlide()
{
	var curPos;
	var slideincnum = getCurrentSlideInc();
	var targetPos = getSlideIncStartPos(slideincnum);
	if (targetPos != null)
	{
		// Only change the player position if it is off by more than one second, to avoid
		// jerky skips in the playback. Also some players do not set the position
		// exactly where you ask it to and can get stuck in a loop; this helps
		// prevent that problem.
		curPos = getPlayerPosition();
		if ( Math.abs(curPos - targetPos) > 1)
			setPlayerPosition(targetPos);
	}
}

// Routine that repeatedly checks the synchronization of the
// slides and player. If the user does not change the slide,
// it advances the slide as the player plays. If the user
// does change the slide, it adjusts the player position.
function checkSync()
{
	var mySlideInc = getCurrentSlideInc();

	// Debugging line, uncomment to help figure out problems with syncing
	//window.alert("mySlideInc " + mySlideInc[0]+","+mySlideInc[1]+" prev "+prevSlideInc[0]+","+prevSlideInc[1]);

	// If user has not changed slides, sync the slide to the player
	if (prevSlideInc[0] == mySlideInc[0] && prevSlideInc[1] == mySlideInc[1])
	{
		syncSlideToPlayer();
		// Record the current slide number for the next pass
		prevSlideInc = getCurrentSlideInc();
	}
	// If the user changed slides, sync the player
	else
	{
		// Check if the player is still loading or otherwise unable to
		// set its position and play.
		// Found that some players will set the position,
		// but then reset to zero after completely loading.
		if (isPlayerReady())
		{
			syncPlayerToSlide();
			
			// Make sure we were able to set the position before resetting
			// the previous slide marker
	
			
			// Check the current player position.
			// If it is null, try to set the player position again next pass
			var curPos = getPlayerPosition();
			if (curPos == null)
				prevSlideInc = new Array(-1,-1);
			else
			{
				// Check to see if the current position is close
				// to where we wanted it.
				// Record the current slide number if so, otherwise
				// flag for another syncPlayerToSlide attempt
				var targetPos = getSlideIncStartPos(mySlideInc);
				if ( Math.abs(curPos - targetPos) < 1)
					prevSlideInc = mySlideInc;
				else
					prevSlideInc = new Array(-1,-1);
			};
		}
		// If the player is not ready, set the previous slide flag
		// to guarantee we will try to sync on the next pass.
		else
			prevSlideInc = new Array(-1,-1);
	}
	
}

