/*****************
 *
 * Created by Dylan Maxwell Reilly http://atariland.net .
 * Feel free to reuse so long as the comments and this header remain intact.
 *
 *****************/

var CIRCLE_MOVE_THRES 	= 1;			//!< Number of pixels of mouse mousement in one "tick."
var CIRCLE_MOVE_DELTA	= 0.025;		//!< Amount to move per "tick" in radians.
var CIRCLE_ANIM_TIME 	= 10;		//!< Time between animation function calls in ms.
var CIRCLE_ANIM_STEP	= 15;		//!< What fraction of the distance to move each frame.

/*!
 * Circle navigation object.
 * Displays images in a circle with the ability to cycle through them and display a larger image
 * and/or text for each. See http://atariland.net/ for an example.
 * \param divBaseWheel The base id name of the DIV elements that contain the wheel images.
 * \param imgBaseWheel The base id name of the IMG elements that define the wheel images.
 * \param divBaseCntr The base id name of the DIV elements that contain the center images.
 * \param imgBaseCntr The base id name of the IMG elements that define the center images.
 * \param divBaseText The base id name of the DIV elements that define the image captions.
 *		Set to null to not display captions.
 */
function CircleNav(divBaseWheel, imgBaseWheel, divBaseCntr, imgBaseCntr, divBaseText, radiusDivisor)
{
	this.divBaseWheel 	= divBaseWheel;
	this.imgBaseWheel	= imgBaseWheel;
	this.divBaseCntr	= divBaseCntr;
	this.imgBaseCntr	= imgBaseCntr;
	this.divBaseText	= divBaseText;

	var pRect = getDocRect();			//!< The current window dimmensions used for centering wheel.
	
	//! radius of the circle.
	var radius 		= pRect.Width / 6;
	//! Additional radius along X for elipse, less radius along Y for elipse.		
	var squish		= new Point(radius, radius / 2);	
	//! Images are zoomed down to a minimum of this size.
	var zoomMin		= 0.25;					
	//! The center of the wheel.
	var wheelPos 		= new Point(pRect.Width / 2, pRect.Height - pRect.Height / 3);
	//! Center of the parge image.
	var largePos		= new Point(pRect.Width / 2, pRect.Height / 2);	
	//! Center of the caption location.
	var textPos	 	= new Point(pRect.Width / 2, pRect.Height - pRect.Height / 8);
	//! The calculated bounding retangle. Reculated when the position or size changes.
	var boundRect 	= null;

	var movable		= true;				//!< Trap mouse movement to move the wheel.
	
	var wheelAngles;						//!< Array of positions of each image along the wheel.
	var imgSizes;							//!< Array of cached sizes of the wheel images.
	var imgCount		= 0;					//!< Number of images in the wheel.
	var cTheta 		= 0;					//!< The current angle that is centered.
	var destTheta		= cTheta;				//!< Destination theta for animations.
	var cImage		= -1;				//!< The currently selected image to be displayed large.
	var pImage		= -1;				//!< The last images to be displayed large.
	var centerDirty	= false;				//!< Need to redraw  the center image.
	
	var mouseDelta	= 0;						//!< Tallies the amount of mouse movement.
	var mouseDown	= false;					//!< Is the mosue button depressed?
	var mouseLast		= new Point(0, 0);		//!< The last known mouse position.
	var angleLast		= 0;					//!< The last known angle of the cursor wrt the wheel center.
	
	var initialized 		= false;			//!< False until ready to be painted.
	
	var angleTotal		= 2 * Math.PI;			//!< Total draw angle around which images will be drawn.
	var flipX			= false;				//!< Flip drawing across X axis.
	var flipY			= false;				//!< Flip drawing across Y axis.
	var zoomEnable	= true;					//!< Enable zooming of images in wheel.
	var fadeLarge		= true;				//!< Cross fade large images when swapping between them.
	var fadeText		= false;				//!< Cross fade text when swapping between them.
	
	var animStep		= 0;					//!< Current anoumt to move each frame. -1 = not animating.
	var animIntr		= null;				//!< Animation function.

	var imgsLoadCB	= null;					//! The callback function passed into load used to track image load progress.
	var imgsToLoad	= 0;						//! Count of total images to be preloaded.
	var imgsLoaded	= 0;						//! Count of total images preloaded so far.
	
	/*!
	 * Sets wheel and large image positions.
	 * \param iWheelPos New position of the center of the wheel as a Point(). null for default.
	 * \param iLargePos New position of the center of the large image as a Point(). null for default.
	 * \param iTextPos New Position of the center of the caption text. null for default.
	 */
	this.setLocations = function(iWheelPos, iLargePos, iTextPos)
	{
		if (iWheelPos != null) 	wheelPos  = iWheelPos;
		if (iLargePos != null) 	largePos	= iLargePos;
		if (iTextPos  != null)		textPos	= iTextPos;
		
		setBoundRect();
		
		if (initialized) paint();
	};
	
	/*!
	 * Returns the location of the center of the wheel as a Point.
	 */
	this.getWheelLoc = function()
	{
		return wheelPos;
	};
	
	/*!
	 * Returns the center of the zero point on the wheel as a Point.
	 */
	this.getZeroLoc = function()
	{
		var x = ((flipX) ? -1 : 1) * (radius * Math.sin(0) + squish.x * Math.sin(0));
		var y = ((flipY) ? -1 : 1) * (radius * Math.cos(0) - squish.y * Math.cos(0));
		return new Point(wheelPos.x + x, wheelPos.y + y);
	};
	
	/*!
	 * Returns the rectangle that bounds the wheel. This does not include the text or 
	 * large images.
	 */
	this.getBoundRect = function()
	{
		return boundRect;
	}
	
	/*!
	 * Sets display parameters.
	 * \param iAngleTotal Total of rotation of the wheel in radians. Math.PI = full circle. 
	 *		null for default.
	 * \param iRadius New radius of the wheel in pixels. null for default.
	 * \param iSquish Change in radius along x and y directions in pixels as a Point() to make
	 *		the wheel oval. null for default.
	 * \param bFlipX Flip the X coordinate of the circle. null for default.
	 * \param bFlipY Flip the Y coordinate of the circle. null for default.
	 * \param bZoomEnable Zoom the images as they move to the rear, true or false. null for default.
	 * \param iZoomMin New minimum image zoom size as a decimal. null for default.
	 */
	this.setDisplayParams = function(iAngleTotal, iRadius, iSquish, bFlipX, bFlipY, bZoomEnable, iZoomMin)
	{
		if (iAngleTotal 	!= null) angleTotal	= iAngleTotal;
		if (iRadius 		!= null) radius 	= iRadius;
		if (iSquish 		!= null) squish	= iSquish;
		if (bFlipX 		!= null) flipX		= bFlipX;
		if (bFlipY 		!= null) flipY		= bFlipY;
		if (bZoomEnable 	!= null) zoomEnable	= bZoomEnable;
		if (iZoomMin 	 	!= null) zoomMin	= iZoomMin;
		
		setBoundRect();
		
		if (initialized) paint();
	};
	
	/*!
	 * Set to cross fade the large images and/or the text.
	 * \param bFadeLarge Perform fades of the large images. null for default.
	 * \param bFadeText Perform fades of the text. null for default.
	 */
	this.setFade = function(bFadeLarge, bFadeText)
	{
		if (bFadeLarge != null) fadeLarge 	= bFadeLarge;
		if (bFadeText   != null) fadeText	= bFadeText;
	}
	
	/*!
	 * Sets the wheel as movabale or not with mouse movement.
	 * \param iMovable New move with mouse value.
	 */
	this.setMovable = function(iMovable)
	{
		movable = iMovable;
	};

	/*!
	 * Image preloading and initial painting.
	 * The parameters to this function exist to handle image preloading. By passing in lists of the
	 * images to load and the name of a callback function, one can provide feeback to users as
	 * to the status of the load.
	 * \param smlImgs Array of small images for preloading. Every odd element in the array must be
	 *	the id of the image and every even element the correct path (as seen from the html) to the image.
	 * 	Optional, pass null to disable.
	 * \param lrgImgs Array of large images for preloading. Same format as smlImgs. Null to disable.
	 * \param callback Name of a function that will be executed as images load. The function must
	 *	take one parameter that will be a value from 0 to 1 that represents the percent complete of
	 * 	loading. The function is guaranteed to recevice both a 0 and a 1 value. Function will receive
	 * 	a -1 on error or abort.
	 */
	this.load = function(smlImgs, lrgImgs, callback) 
	{
		var theta;
		var cntrCount 	= 0;
		var textCount 	= 0;
		var loadStyle;

		// Call the callback once with 0 to begin. 
		// If it fails, perform no more callbacks.
		// Remeber the callback so that progress can be updated.
		if (callback)
		{
			try 
			{
				eval(callback + "(0)");
				imgsLoadCB = callback;
			}
			catch (e)
			{
				alert("Callback function " + callback + " is invalid.");
				callback = null;
			}
		}
		
		// Attempt to get a count of the number of wheel images in the page.
		while(true)
		{
			try 
			{
				var item = getItem(divBaseWheel + imgCount);
				if (item == null) break;
				imgCount++;
				
				// Count DIV's of center images.
				if (divBaseCntr != null)
				{
					item = getItem(divBaseCntr + (imgCount - 1));
					if (item != null) cntrCount++;
				}
				// Count DIV's of captions.
				if (divBaseText != null)
				{
					item = getItem(divBaseText + (imgCount - 1));
					if (item != null) textCount++;
				}
			}
			// Avoid infinite loops just in case something unexpected happens.
			catch (e) 
			{
				break;
			}
		}
		
		// We need to find at least one definition for 1 wheel image.
		if (imgCount == 0) 
		{
			alert("No wheel image DIV's found.");
			return;
		}
		// Sanity checks to see if user wanted text and large images but did not define enough in the HTML.
		if (divBaseCntr != null && cntrCount != imgCount)
			alert("There are " + imgCount + " wheel images defined, but only " + cntrCount + " center images.");
		if (divBaseText != null && textCount != imgCount)
			alert("There are " + imgCount + " wheel images defined, but only " + textCount + " captions.");
		// See if the correct number of preload image definitions were passed.
		if (smlImgs != null && smlImgs.length < imgCount * 2)
			alert("Small image list passed to load() incorrect. Expect " + imgCount*2 + " element but got " + smlImgs.length);
		if (divBaseCntr != null && lrgImgs != null && lrgImgs.length < cntrCount * 2)
			alert("Image list passed to load() incorrect. Expect " + cntrCount*2 + " element but got " + lrgImgs.length);
		
		// Now, build arrays of the image properties for east access later.
		theta 		= angleTotal / imgCount;	// Angle between the images.
		wheelAngles 	= new Array(imgCount);
		imgSizes		= new Array(imgCount);

		imgsToLoad 	= imgCount + cntrCount;	// The tally of the total number of images that must be loaded.

		// Calculations and preloading for wheel images.
		for (a = 0; a < imgCount; a++)
		{
			var image 		= getImageStyle(imgBaseWheel + a);
			// Position each image around the wheel evenly.
			wheelAngles[a] 	= theta * a;
			// Cache the image size.
			imgSizes[a] 		= new Point(parseInt(image.width), parseInt(image.height));

			// Preload the image into memory.
			if (smlImgs != null && smlImgs.length > a * 2)
			{
				image		= getItem(smlImgs[a*2]);	// Get the image to load.
				image.onload 	= imageLoadCB;			// Set a callback so we know when it completes.
				image.onerror	= imageErrorCB;
				image.onabort 	= imageAbortCB;
				image.src 	= smlImgs[a*2 + 1];		// Set the source to to the correct image.
			}
		}

		// Preloading for large images.
		for (a = 0; lrgImgs != null && a < cntrCount; a++)
		{
			if (lrgImgs.length > a * 2)
			{
				image	 	= getItem(lrgImgs[a*2]);
				image.onload	= imageLoadCB;
				image.onerror	= imageErrorCB;
				image.onabort 	= imageAbortCB;
				image.src		= lrgImgs[a*2 + 1];
			}
		}

		// If no callback was given, asume we should paint immediately. Otherwise, 
		// paint only after all images are loaded.
		if (callback == null)	
		{
			paint();
			initialized = true;
		}
	};
	
	/*!
	 * The action to perform when an image completes loading. Set as the onload
	 * event in an image during the load pahse if using image preloading.
	 * Will call to the callback function passed into the load function if defined.
	 */
	function imageLoadCB()
	{
		imgsLoaded++;
		
		if (imgsLoadCB)
			eval(imgsLoadCB + "(" + imgsLoaded / imgsToLoad + ")");
		
		// Done. Paint and set fully initialized.
		if (imgsLoaded == imgsToLoad)
		{
			paint();
			initialized = true;
		}
	}
	
	/*!
	 * Callback for image load failure.
	 * Displays an error message, sends error code to callback, and paints display.
	 */
	function imageErrorCB()
	{
		
		if (imgsLoadCB != null)
		{
			alert("One or more images for preload could not be found. Aborting.");
			eval(imgsLoadCB + "(-1)");
			imgsLoadCB = null;
			paint();
		}
	}
	
	/*!
	 * Callback for image load user abort.
	 * Sends error code to callback, and paints display.
	 */
	function imageAbortCB()
	{
		if (imgsLoadCB)
		{
			eval(imgsLoadCB + "(-1)");
			imgsLoadCB = null;
			paint();
		}
	}
	
	/*!
	 * Animate wheel rotation.
	 * This method should be called for all wheel movement. Set destTheta to the desired
	 * destination angle and this method does all the work.
	 */
	function animate()
	{
		// Do not animate if no where to go.
		if (destTheta == cTheta)  return;
		
		// animStep == 0 means not animating.
		if (animStep != 0) return;
		
		// Determine the optimal direction to travel based upon the diffence between the two angles.
		animStep = destTheta - cTheta;
		if (animStep >  angleTotal / 2) animStep =  animStep - angleTotal;
		if (animStep < -angleTotal / 2) animStep = angleTotal + animStep;
		animStep /= CIRCLE_ANIM_STEP;

		// User setInterval to animate.
		animIntr = window.setInterval
		(
			function() 
			{
				// Break when we are within a delta of the destination.
				if (Math.abs(destTheta - cTheta) <= Math.abs(animStep) || Math.abs(destTheta - cTheta) >= angleTotal) 
				{
					window.clearInterval(animIntr);		// Remove the interval updater.
					animStep	= 0;					// Stop animating.
					cTheta	= destTheta;			// Set the final destiantion.
					cTheta   %= angleTotal;			// Constrain to complete circle.
				}
				else
				{
					cTheta += animStep;			// Advance the current angle. Constrain to full circle.
					cTheta %= angleTotal;			// Constrain to complete circle.
					if (cTheta < 0) cTheta += angleTotal;		
				}

				paint();
			},
			CIRCLE_ANIM_TIME
		);
		
	}

	/*!
	 * Paint it.
	 */
	function paint()
	{
		// Draw the small image ring.
		paintRing();

		// Paint the center image and captions. Do not draw while animating. We only want to change the image
		// when the wheel stops. Painting it all the time is sloooooow.
		if (animStep == 0) 
		{
			findCurrentImage();
			paintCenter();
		}
	}
	
	/*!
	 * Paints the center image and captions.
	 */
	function paintCenter()
	{
		// Draw the large image.
		if (centerDirty)
		{
			if (divBaseCntr != null)
			{
				// Show the current one.
				if (fadeLarge)
					alphaFade(divBaseCntr + cImage, 0, 10, 500, -1);
				else
					getItemStyle(divBaseCntr + cImage).visibility = "visible";
				// Hide the previous one.
				if (pImage != -1 && cImage != pImage)
				{
					if (fadeLarge)
						alphaFade(divBaseCntr + pImage, 10, 0, 400, -1);
					else
						getItemStyle(divBaseCntr + pImage).visibility = "hidden";
				}
				divStyle 		= getItemStyle(divBaseCntr + cImage);
				divStyle.zIndex = 90;
				imgStyle 		= getImageStyle(imgBaseCntr + cImage);
				// Center the image in the ring.
				divStyle.left	= largePos.x - parseInt(imgStyle.width)  / 2;
				divStyle.top  	= largePos.y - parseInt(imgStyle.height) / 2;
			}
			
			// Draw captions.
			if (divBaseText != null)
			{
				textStyle 		= getStyle(getItem(divBaseText + cImage));
				// Center the text at the set coordinates. Must use offsetWidth to get the computed width.
				textStyle.left	= textPos.x - parseInt(getItem(divBaseText + cImage).offsetWidth) / 2;
				textStyle.top 	= textPos.y;
				if (fadeText)
					alphaFade(divBaseText + cImage, 0, 10, 350, -1);
				else
					getItemStyle(divBaseText + cImage).visibility = "visible";
				// Hide the previous one.
				if (pImage != -1 && cImage != pImage)
				{
					if (fadeText)
						alphaFade(divBaseText + pImage, 10, 0, 350, -1);
					else
						getItemStyle(divBaseText + pImage).visibility = "hidden";
				}
					
			}
			
			// Remeber this image as the new "last" image.
			pImage = cImage;
		}
		else if (! centerDirty && pImage != -1)
		{
			if (divBaseCntr != null)
			{
				divStyle = getStyle(getItem(divBaseCntr + pImage));
				divStyle.visibility = "hidden";
			}
			
			if (divBaseText != null) 
				getStyle(getItem(divBaseText + pImage)).visibility = "hidden";
		}
		
		// Do not draw again until current image is recaluclated to save processing time.
		centerDirty = false;
	}

	/*!
	 * Draws the small image ring.
	 */
	function paintRing()
	{	
		var divStyle, imgStyle;

		for (var i = 0; i < imgCount; i++)
		{
			var theta 	= (cTheta +  wheelAngles[i]) % angleTotal;
			var x		= ((flipX) ? -1 : 1) * (radius * Math.sin(theta) + squish.x * Math.sin(theta));
			var y		= ((flipY) ? -1 : 1) * (radius * Math.cos(theta) - squish.y * Math.cos(theta));
			var zoom		= (zoomEnable) ? (1 + Math.cos(theta)) / 2 : 1;
			zoom 		= (zoom < zoomMin) ? zoomMin : zoom;
		
			divStyle = getItemStyle(divBaseWheel + i);		

			// Dynamic z indexing based on distance from center. Allows images
			// to disappear behind the other images as the move to the background.
			divStyle.zIndex 	= Math.floor(Math.abs(180 - theta * 180 / Math.PI));

			divStyle.visibility 	= "visible";
			imgStyle			= getImageStyle(imgBaseWheel + i);
			imgStyle.width		= imgSizes[i].x * zoom;
			imgStyle.height	= imgSizes[i].y * zoom;
			divStyle.left 		= wheelPos.x + x - parseInt(imgStyle.width)  / 2;
			divStyle.top 		= wheelPos.y + y - parseInt(imgStyle.height) / 2;
		}
	}
	
	/*!
	 * Finds the currently selected (at position 0) image and sets it in the variable cImage.
	 * The center image will not draw unless cImage is set.
	 */
	function findCurrentImage()
	{
		var delta 		= angleTotal / imgCount / 2;
		centerDirty	= false;
		
		for (var i = 0; i < imgCount; i++)
		{
			var theta = (cTheta +  wheelAngles[i]) % angleTotal;
			// Need to account for rounding errors. Idealy, theta should = 0 for the selected image.
			// But, that is not always the case.
			if (theta >= angleTotal - delta / 2 || theta <= delta / 2)
			{
				cImage 		= i;
				centerDirty 	= true;
				return;
			}
		}
	}

	/*!
	 * Move the wheel in concise steps. Each step is one position around the wheel.
	 * \param step Number of steps to move. May be negative.
	 */
	this.go = function(step)
	{
		// do not update destination theta if currently animating to one.
		if (animStep != 0) return;

		destTheta = cTheta + step * angleTotal / imgCount;
		if (destTheta < 0) destTheta += angleTotal;
		if (destTheta > angleTotal) destTheta -= angleTotal;

		animate();
	};
	
	/*!
	 * Move the wheel in concise steps. Each step is one position around the wheel.
	 * \param step Number of steps to move. May be negative.
	 */
	function myGo(step)
	{
		// do not update destination theta if currently animating to one.
		if (animStep != 0) return;
		
		destTheta = cTheta + step * angleTotal / imgCount;
		if (destTheta < 0) destTheta += angleTotal;
		if (destTheta > angleTotal) destTheta -= angleTotal;

		animate();
	}
	
	/*!
	 * Sets the current icon number as the selected one.
	 */
	this.setSelected = function(iconNum)
	{
		cTheta = iconNum * angleTotal / imgCount;
		paint();
	};

	this.onMouseDown = function(e)
	{
		var centerItemRect = getCenterItemRect();
		
		mouseCur = getMouseXY(e);	// Current mouse location.
		
		// Only handle mouse movement inside the bounding rectangle.
		if (boundRect.contains(mouseCur) 
			// And not inside the item in the middle of the circle.
			&& ! centerItemRect.contains(mouseCur))
		{
			mouseDelta	= 0;
			mouseDown 	= true;
			// Returning false will tell the browser we handled the request and it will ignore its own processing.
			return false;
		}
		
		mouseDown = false;
		return true;
	};

	this.onMouseUp = function(e)
	{
		var mouseWasDown = mouseDown;
		
		mouseDelta 	= 0;
		mouseDown	= false;
		
		return ! mouseWasDown;
	};

	/*!
	 * Handles all mouse movement.
	 * The wheel is advanced or retreated by one step whenever the mouse moves over
	 * a certain threshold of distance in one direction (clockwise or counter-clockwise).
	 */
	this.onMouseMove = function(e)
	{
		var mouseCur, angleCur, delta;
	
		// Only handle mouse movement when the button is down, movement is enabled, and
		// the wheel is not animating.
		if (! mouseDown || ! movable) return true;
	
		mouseCur = getMouseXY(e);	// Current mouse location.
		
		// Only handle mouse movement inside the bounding rectangle.
		if (! boundRect.contains(mouseCur)) return true;

		// Determine the total distance moved and tally it.
		delta		= Math.sqrt(sqr(mouseCur.x - mouseLast.x) + sqr(mouseCur.y - mouseLast.y));
		mouseLast 	= mouseCur;
	
		// Determing the current angle of the mouse around the center of the wheel.
		angleCur = Math.atan((mouseCur.x - wheelPos.x) / (mouseCur.y - wheelPos.y));
		// Move the wheel based on the difference in the current angle versus the last recorded angle.
		mouseDelta += (angleLast - angleCur < 0) ? CIRCLE_MOVE_DELTA : -CIRCLE_MOVE_DELTA;

		// Draw the wheel at the new position.
		if (Math.abs(mouseDelta) >= CIRCLE_MOVE_THRES)
		{
			// Advance one position around the wheel.
			myGo((mouseDelta < 0) ? -1 : 1);
			// Reset the tally of minimum mouse movement.
			mouseDelta = 0;
		}
	
		angleLast	= angleCur;	// Save the current angle for comparison next time.
		
		return false;
	};
	
	function setBoundRect()
	{
		boundRect = new Rectangle(wheelPos.x - radius * 1.25 - squish.x * 2, wheelPos.y - radius - squish.y / 2,
								  radius * 2.5 + squish.x * 2, radius * 2 + squish.y);

	}
	
	/*!
	 * Gets the bouding rectangle of the item in the center of the wheel whether it me an image or some HTML.
	 * \return Rectangle of item in center.
	 */
	function getCenterItemRect()
	{
		var centerRect = new Rectangle(0, 0, 0, 0);
		
		// Center item is an image.
		if (cImage >=0 && divBaseCntr != null)
		{
			var img		= getItem(imgBaseCntr + cImage);
			var div		= getItem(divBaseCntr + cImage);
			centerRect.x 	= div.offsetLeft;
			centerRect.y 	= div.offsetTop;
			centerRect.dx 	= img.offsetWidth;
			centerRect.dy 	= img.offsetHeight;
		}
		else if (divBaseText != null) 
		{
			var item		= getItem(divBaseText + cImage);
			centerRect.x 	= item.offsetLeft;
			centerRect.y 	= item.offsetTop;
			centerRect.dx 	= item.offsetWidth;
			centerRect.dy 	= item.offsetHeight;
		}
		
		return new Rectangle(centerRect.x, centerRect.y, centerRect.dx, centerRect.dy);
	}
}


