/** 
 * @fileoverview MultiThumbSlider Widget - construct a DHTML {@link Phoenix.Widgets.Slider} widget with support for an arbitrary number of thumbs.  Instantiates itself on the page and gracefully degrades back to the DHTML fallback content you specify.  Exposes several event handlers so you can hook into widget-specific events like "thumb dragged" from your own code.
 *  
 * @author James Palmer james@phoenixlondon.co.uk
 * @version 0.1 
 */

// ***** Require External Dependencies *****
requireNamespace("Phoenix.Widgets.Slider");

/**
 * Construct the Phoenix.Widgets.Slider.MultiThumbSlider object.
 * @class Defines the MultiThumbSlider DHTML widget
 * @constructor
 * @requires Phoenix.Widgets.Slider
 * @base Phoenix.Widgets.Slider
 */
Phoenix.Widgets.Slider.MultiThumbSlider = function(container, insertionmethod)
{
	this.nsIdentifier = this.nsIdentifier || "Phoenix.Widgets.Slider.MultiThumbSlider";
	
	// Object inheritance hackery:
	this.base = Phoenix.Widgets.Slider;
	this.base(container, insertionmethod);
	
	this.selectedThumb = null;
	this.lastChangedThumb = null;
	this.thumbs = new Array();						// Define Array of thumbs instead of a single hard-coded variable
	this.minThumbSeparationFraction = 30;			// The minimum permitted distance between thumbs on the slider, expressed as the divisor of a fraction of the full slider width.  IE, a value of "30" means thumbs may get no closer than 1/30th of the total slider width. 
 	
 	
	this._render = function()
	{
		// Build new elements & save references to them for later manipulation
		
		if(!this.rendered)
		{
			if(this.renderMethod == 0)		// Add the new elements to the end of the div
				this.rootElement.appendChild(this.background);
			else													// Add the new elements to the beginning of the div (insertionmethod == 1-3)
				this.rootElement.insertBefore(this.background, container.firstChild);
			
			this.rendered = true;
		}
		
		this.setNumTicks(this.numTicks);

		// Set position of sliders to reflect their internal state
		for(var i=0; i<this.thumbs.length; i++)
			this.setThumbValue(this.thumbs[i], this.thumbs[i].prerenderedValue);
	};
	
	// Given a thumb index or a reference to a thumb object, return a hash with both the index *and* the DOM node reference in
	this.getThumb = function(thumb)
	{
		var noderef = null;
		var index = 0;
		
		if(typeof(thumb) != "object" && typeof(thumb) != "number")
			return(false);
		
		if(typeof(thumb) == "object")	// thumb node reference
		{
			noderef = thumb;
			while(index<this.thumbs.length && this.thumbs[index] != noderef)
				index++;
			
			if(index == this.thumbs.length)	// handles array overruns as well as empty arrays
				return(false);
		}
		else													// integer index
		{
			if(index >= this.thumbs.length)
				return(false);

			index = thumb;
			noderef = this.thumbs[index];
		}
		
		return({nodeRef:noderef, index:index});

	};
	
	// Add new methods and properties for multi-thumb slider
	this.addThumb = function(numthumbs)
	{
		numthumbs = numthumbs || 1;
		
		var newthumb = null;
		while(numthumbs > 0)
		{
			var newthumbvalue = 0;
			
			if(this.thumbs.length > 0)
			{
				var prevthumb = this.thumbs[this.thumbs.length-1];	// Grab thumb immediately before this one
				
				// New thumb's value is previous thumb's value + min separation between thumbs
				newthumbvalue = prevthumb.prerenderedValue + ((this.maxValue - this.minValue) / this.minThumbSeparationFraction);
				
				if(newthumbvalue > this.maxValue)	// If new thumb's value exceeds max value of slider, bail out with an error condition
					return(false);
			}
			// else this is the first slider thumb, so leave newthunmbvalue at 0


			// Create new thumb
			newthumb = document.createElement("div");
			newthumb.className = "thumb";
			newthumb.owningControl = this;
			newthumb.onmousedown = function (event)
			{
				if(this.owningControl.onthumbmousedown)
					this.owningControl.onthumbmousedown(event);
			};
			this.background.appendChild(newthumb);	// Append the new thumb to the control
			
			if(this.rendered)		// If control has been rendered, attempt to move thumb to the position indicated by its value
			{
				var newthumbxy = this.sliderValueToRelativeXY(newthumbvalue);
			
				var newcoords = this.moveSliderMiddleToRelativeXY(newthumbxy.x, 0, newthumb);	// Snap to grid (for nasty rounding errors when adding several thumbs one after the other)
				
				newthumb.prerenderedValue = this.relativeXYToSliderValue(newcoords.x, newcoords.y);	// and just in case anything happens
			}
			else
				newthumb.prerenderedValue = newthumbvalue;
			
			this.thumbs.push(newthumb);	// and add the new thumb into the "thumbs" array
			
			numthumbs--;				// And decrement "number of thumbs to add"
		}
		
		return(newthumb);	// Return the last thumb created (for convenience, and want of anything better to return)
	};
	
	
	// Remove a thumb
	this.removeThumb = function(thumb)
	{
		var thumbdetails = this.getThumb(thumb);
		if(!thumbdetails)
			return(false);
		
		var oldthumb = this.background.removeChild(thumbdetails.nodeRef);
		this.thumbs.splice(thumbdetails.index, 1);
		
		return(oldthumb);
	};


	this.setThumbValue = function(thumb, newval)
	{
		var thumb = this.getThumb(thumb);	// Can pass in an index or a noderef - ensure no matter which is passed in we get a valid noderef out of it
		if(!thumb)
			return(false);
		thumb = thumb.nodeRef;

		// Sanity-check slider min/max values to ensure we're returning something reliable
		if(this.minValue > newval || newval > this.maxValue || this.minValue >= this.maxValue)
			return(false);
			
		this.lastChangedThumb = this.selectedThumb;
		this.selectedThumb = thumb;

		var allowedrange = this.getAllowedRange(thumb);
		if(newval < allowedrange.low)
			newval = allowedrange.low;
		if(newval > allowedrange.high)
			newval = allowedrange.high;
		
		thumb.prerenderedValue = newval;	// update internal state of thumb to its new value
		
		if(this.rendered)					// and if the control's been rendered, move the appropriate div marker
		{
			var newcoords = this.sliderValueToRelativeXY(newval);
			this.moveSliderMiddleToRelativeXY(newcoords.x, newcoords.y, thumb);
		
			// Move slider to appropriate position
			//thumb.style.left = newcoords.x + "px";
				
			// And finally fire change event to notify any attached event handlers that the value's changed
			if(document.createEvent)
			{
				var event = document.createEvent("HTMLEvents");
				event.initEvent("change", true, true);
				thumb.dispatchEvent(event);
			}
			else if(thumb.fireEvent)
			{
				thumb.fireEvent("onclick");	// IE balks at firing a change event, so we compromsie with a less-accurate (but supported) "click" event
			}
		}
				  
		return(newval);
	};
	

	this.getClosestThumbToValue = function(value, direction, ignorethumb)
	{
		// Ensure direction is -1, 0 or 1 (closest thumb with value lower than passed value, closest thumb with value higher than passed value, or thumb with absolute closest value to passed value) 
		if(direction != -1 && direction != 0 && direction != 1)
			direction = 0;
		
		if(this.thumbs.length <= 0)	// If no thumbs, return false ("there is no thumb" ;-)
			return(false);
		
		var closestthumb = null;									// Closest thumb we've found so far
		var closestthumbdist = this.maxValue - this.minValue;		// Shortest distance to nearby thumb we've seen so far

		for(var index in this.thumbs)									// Iterate over thumbs array
		{
			var thisthumbvalue = this.thumbs[index].prerenderedValue; 
			var thisdistance = Math.abs(value - thisthumbvalue);	// Work out distance between this thumb's value and the specified value
			
			if(closestthumbdist >= thisdistance)						// If it's closer than the shortest distance we've seen so far... (greater-than-or-EQUALS to handle the edge-case where we have one other thumb at the opposite end of the scale - closestthumbdist is initialised to the length of the scale (a sensible large default), but in this case that's the same distance as the only other thumb.  If we use >= then the highest bound is determined to be this other thumb, not just the end of the scale. 
			{
				if(((direction == 1 && thisthumbvalue > value) ||	// ... and it's in the specified direction (higher, lower or either)...  
					(direction == -1 && thisthumbvalue < value) ||
					(direction == 0)) &&
					(this.thumbs[index] != ignorethumb))					// ... and it's not any thumb we've specifically been told to ignore (eg, if we're trying to find the closest thumb to an existing thumb's position)
				{
					closestthumbdist = thisdistance;				// Then record this as the new closest thumb
					closestthumb = this.thumbs[index];
				}
			}
		}
		
		return(closestthumb);
	}
	
	
	
	this.getClosestThumbToXY = function(x,y, direction, ignorethumb)			// For convenience - just a thin wrapper around getClosestThumbToValue()
	{
		var value = this.relativeXYToSliderValue(x, y);
		if(value)
			return(this.getClosestThumbToValue(value));
		// else
		return(false);
	};



	// Return the allowed limits of movement for this thumb (can't go outside slider background area or closer than this.minDistanceBetweenThumbs to other thumbs)	
	this.getAllowedRange = function(thumb)
	{
		// Now, to stop thumbs moving past each other we have to grab the thumbs (if any) either side of the selectedThumb and compare their positions.
		// There are three ways we could do this:
		// 1. Grab selectedThumb.previousSibling and selectedThumb.nextSibling from the DOM.  Quick, but relies on the DOM nodes being in the appropriate order (which may change in the future).
		// 2. Grab the nodes either side of selectedthumb in the MultiThumbSlider.thumbs array.  Slow (grabbing a node's index given a node reference requires iterating over the array), but only relies on the array entries being in the appropriate order (which is less likely to change in the future).
		// 3. Iterate over every node in the MultiThumbSlider.thumbs array and grabbing the next-highest and next-lowest thumbs.  Slowest of all (iterate entire array once or twice every time, versus an average of half the array with option 2), but most reliable - nodes/array entries may be in any order, so much more future-proof.
	
		var thumbvalue = thumb.prerenderedValue;
		var closestlowerthumb = this.getClosestThumbToValue(thumbvalue, -1, thumb);	// returns null if none found
		var closesthigherthumb = this.getClosestThumbToValue(thumbvalue, 1, thumb);
	
		var minseparation = (this.maxValue - this.minValue) / this.minThumbSeparationFraction;
		
		var lowerlimit = closestlowerthumb ? closestlowerthumb.prerenderedValue+minseparation: 0;
		var upperlimit = closesthigherthumb ? closesthigherthumb.prerenderedValue-minseparation: this.maxValue;
		
		return({low:lowerlimit, high:upperlimit});
	};
	
			// Attach event handlers
		
		this.background.onmousedown = function (event)
		{
			requireNamespace("Phoenix.Util.Mouse");
			
			event = event || window.event;
			this.owningControl.mouseIsDown = true;
						
			var newcoords = Phoenix.Util.Mouse.getRelativePosition(event, this.owningControl.background);
			var newvalue = this.owningControl.relativeXYToSliderValue(newcoords.x, newcoords.y);	// slider value that's represented by the x,y co-ordinates of the mouseclick
			var nearestthumb = this.owningControl.getClosestThumbToValue(newvalue);
			this.owningControl.setThumbValue(nearestthumb, newvalue);
			

			this.onmousemove = function(event)
			{
				requireNamespace("Phoenix.Util.Mouse");
			
				if(this.owningControl.mouseIsDown)	// This event handler should only ever *exist* when the mouse is down (to save on CPU-thrashing), but it's always better to be safe than sorry...
				{
					var newcoords = Phoenix.Util.Mouse.getRelativePosition(event, this.owningControl.background);
					var newvalue = this.owningControl.relativeXYToSliderValue(newcoords.x, newcoords.y);	// slider value that's represented by the x,y co-ordinates of the mouseclick
					var nearestthumb = this.owningControl.getClosestThumbToValue(newvalue);
					this.owningControl.setThumbValue(nearestthumb, newvalue);
				}
				
				// Fire any user-supplied event handlers
				if(this.owningControl.ondrag)
					this.owningControl.ondrag(event);
			};
			
			if(this.owningControl.onbackgroundmousedown)
				this.owningControl.onbackgroundmousedown(event);
				
			if(this.owningControl.onmousedown)
					this.owningControl.onmousedown(event);
		};
		
		this.background.onmouseup = function(event)
		{	// this == Phoenix.Widgets.Slider.thumb;
			
			this.owningControl.mouseIsDown = false;
			this.owningControl.lastChangedThumb = this.owningControl.selectedThumb;
			this.owningControl.selectedThumb = null;
								
			// Clear mousemove event handler onmouseup, or it thrashes the CPU handling every single mousemove event in the window. 
			this.onmousemove = null;
			
			// Fire any user-supplied event handlers
			if(this.owningControl.onmouseup)
				this.owningControl.onmouseup(event);
				
			if(this.owningControl.onclick)
				this.owningControl.onclick(event);

			if(this.owningControl.onchange && this.owningControl.hasBeenDragged == true)
			{
				this.owningControl.onchange(event);
				this.owningControl.hasBeenDragged = false;	// and reset value for next time
			}
		};
		
		this.background.onchange = function(event)
		{
			if(this.owningControl.onchange)
				this.owningControl.onchange(event);
		};
		

	// We're descended from a slider control with only one thumb slider, so remove/replace the various bits we need to in order for us to support work with multiple thumbs
//	document.removeChild(this.thumb);	// Don't need to destroy old thumb as it'll never be created/rendered
	this.thumb = null;
	this.addThumb(1);
};
