/** 
 * @fileoverview Slider Widget - construct an imagemap with highlighted toggle/selectable areas.  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 "area selected" from your own code.
 *  
 * @author James Palmer james@phoenixlondon.co.uk
 * @version 0.1 
 */

// ***** Require External Dependencies *****
requireNamespace("Phoenix.Widgets", "Phoenix.Widgets.Slider");

/**
 * Construct the Phoenix.Widgets.Slider object.
 * @class Defines the Slider DHTML widget
 * @constructor
 * @requires Phoenix.Widgets
 */
Phoenix.Widgets.Slider = function(container, insertionmethod)
{
	// IMPORTANT: All internal functions use a scale of 0-1 inclusive.  Functions designed for external use convert these values from 0-1 to minValue-maxValue for convenience.

	this.nsIdentifier = this.nsIdentifier || "Phoenix.Widgets.Slider";	// Protect in case of subclassed controls
	
	// ***** Require External Dependencies *****
	requireNamespace("Phoenix.Widgets", this.neIdentifier);
	requireNamespace("Phoenix.DOM", this.neIdentifier);
	requireNamespace("Phoenix.Util.Mouse", this.neIdentifier);
	
	// ***** Sanity-Check Passed Parameters *****
	
	if(!container) throw new Phoenix.Exception("Phoenix.Widgets.Slider.Constructor: First parameter must be a valid HTML element reference.");	// Can't survive without an element to bed ourselves down in
	insertionmethod = (insertionmethod == null || insertionmethod > 3 || insertionmethod < 0) ? 3: insertionmethod;			// If insertionmethod not specified or invalid, assume "unrender fallback elements and insert ourselves *before* them in the div"

	// ***** State Variables *****
	
	// Set up identifying properties of this widget
	this.globalWidgetIndex = Phoenix.Widgets.registerThisWidget(this.nsIdentifier, this);	// Grab a reference to the index of this particular instance of the control in the Phoenix.Widgets._InstantiatedControls array (in case we ever need to refer to ourself in the global scope)
	
	this.renderMethod = insertionmethod;	// How are we to render ourselves?  See Render() for explanation of values.
	this.rendered = false;					// Has this control been rendered yet?
	this.minValue = 0;							// Minimum value for the slider to return
	this.maxValue = 1;							// Maximum value for the slider to return
	this.numTicks = 5;							// Number of "tick marks" to snap the slider to
	this.mouseIsDown = false;			// Is the mouse currently down?  (Mostly used in event handlers)
	this.hasBeenDragged = false;	// Has the slider been dragged  (Mostly used in event handlers)

	// ** "Child HTML Element" references **
	// Root element of this control:
	this.rootElement = container;						// Reference to the root element that this widget inhabits
	this.rootElement.owningControl = this;

	//  Slider background
	this.background = document.createElement("div");	// Build any required child elements and save references to them
	this.background.className = "background";
	this.background.owningControl = this;
	
	// Slider thumb
	this.thumb = document.createElement("div");
	this.thumb.className = "thumb";
	this.thumb.owningControl = this;
	this.thumb.prerenderedValue = 0;			// In case we need to set a value on the div before we render it (and hence before we know its width - necessary to calculate an actual pixel position), allow us to set a value (in the range 0-1) for the div to be initially rendered at.  The value in the range 0-1 will be mapped to an actual position on the slider on rendering.
	
	// Set up event-handler callbacks (these are events that the slider control exposes *to other scripts*.  Its constituent elements will have their own (private) event handlers below that call any function attached to these properties after they do their jobs).
	this.onthumbmousedown = null;			// Fired when the thumb slider is left-clicked (ie, thumb.onmousedown)
	this.onbackgroundmousedown = null;	// Fired when the background div is left-clicked (ie, background.onmousedown)
	this.onmousedown = null;						// Fired when either the thumb or the slider is left-clicked.  If the thumb is clicked the order of mousedown events triggered is thumb->background->slider.
	this.ondrag = null;								// Fired when the control has been dragged to a new position (ie, onmousemove when mousedown).  Doesn't matter if the original mousedown was on the thumb or the background element.
	this.onmouseup = null;							// Fired when the mouse button is raised
	this.onclick = null;							// Fired when the mouse button is raised 
	this.onchange = null;							// Fired when mouseup after the thumb slider has changed position

	
	// ***** Do Initial "Pre-Rendering" Setup *****

	// Mark out this element's root container as a mapwidget (for styling purposes)
	Phoenix.DOM.addCSSClass(this.rootElement, "sliderwidget");

	// If appropriate, hide or unrender any DHTML fallback elements in the div
	// We do this here so that it'll be done while the DOM is still loading, rather than having them flicker by waiting for the entire page to load (exposing the fallback elements the whole time) and then blanking them out after a short delay
	if(this.renderMethod == 2 || this.renderMethod == 3)	// If hide/undisplay all fallback elements
	{
		if(container.hasChildNodes())
		{
			var removemethod = (this.renderMethod == 2) ? "hidden": "unrendered";	// Remove (display:none) the control or just hide (visibility:hidden) it?
	
			// Hide all "fallback" HTML elements (anything inside the div this control has been passed as its container)
			//var children = container.getElementsByTagName("*");
			var children = Phoenix.DOM.getElementsByClassName("dhtmlfallback", container);
			for(var i=0; i<children.length; i++)	// remove/hide old HTML elements
				Phoenix.DOM.addCSSClass(children[i], removemethod);
		}
	}
	
	
	// ***** Internal Functions *****
	
	/**
	 * Render this widget
	 * @private
	 */
	this._render = function()
	{
		/*
			Add new controls to parent rootElement in one of four ways:
			1a. Leave previous contents alone and append to end of div (this.renderMethod == 0)
			1b. Leave all previous content alone and prepend to beginning of div (this.renderMethod == 1)
			2a. Set all previous contents of the div to visibility:hidden and prepend to beginning of the div (this.renderMethod == 2)
			2b. Set all previous contents of the div to display:none and prepend to beginning of the div (this.renderMethod == 3)
		*/

		// 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);

			if(this.thumb.parentNode != this.background)
				this.background.appendChild(this.thumb);

			this.rendered = true;
		}

		this.setNumTicks(this.numTicks);
		
		// Set position of slider now it's been rendered
		this.setValue(this.thumb.prerenderedValue);
		
		// Mozilla has a bug where under certain circumstances - although the offsetWidth is still correctly reported - it will fail to render the background image all the way across the background div, leaving a 1-pixel line of transparent pixels down the right-hand edge of the slider and making it look as if the slider thumb has slid off the end of the background.  For some reason, setting the STYLE.width to the same value as the (correctly-reported) offsetWidth solves this rendering bug.  Go figure.
		//this.background.style.width = this.background.offsetWidth + "px";
	};

	this.setNumTicks = function(numticks) {

		//debugger

		// Separate width of slider into equal divisions, and put a tick-mark at the left of each division

		if(numticks < 2)	// Limit case is two ticks (left and right, nothing in the middle), or if the number of ticks hasn't changed, also don't do anything
			return(null);

		// Save new number of ticks
		this.numTicks = numticks;


		// If we aren't rendered, that's all we need to do
		if(!this.rendered) {
			return(true);
		}

		// If we are rendered, remove old ticks/create new ones:

		//debugger
		
		// Clear out any old ticks:
		var oldticks = Phoenix.DOM.getElementsByClassName("backgroundtick", this.background);
		for(var i=0; i<oldticks.length; i++) {
			this.background.removeChild(oldticks[i]);
		}

		// And create new ticks
		var ticksize = 1/(numticks-1);	// Everything's done on an internal scale of 0-1, then up-converted on display

		for(var i=0; i<numticks; i++) {
			var newtick = document.createElement("div");
			this.background.appendChild(newtick);

			newtick.style.position = "absolute";
			newtick.className = "backgroundtick";
			
			if(i == 0) {
				newtick.className += " first";
			}

			if(i == numticks-1) {
				newtick.className += " last";
			}
			
			newtick.style.height = Phoenix.Util.lengthToPX(Phoenix.DOM.getComputedStyle("height", this.background), this.background) + "px";
			newtick.style.left = (i*ticksize*100) + "%";
			
			if(i == numticks-2) {	// Penultimate tick - ensure we avoid rounding errors
			
				var totalwidth = this.background.offsetWidth;
				
				var occupied_width = Math.round(parseFloat(newtick.style.left)*totalwidth/100);

/*				console.log("Left: "+occupied_width);
				console.log("Background width: "+totalwidth);*/
				
				newtick.style.width = ((totalwidth-1)-occupied_width)+"px";
			}
			else if(i != numticks-1) {	// Last tick overhangs the end of the slider bar (so it can appear to finish on a tick mark), and so may be shortened in the stylesheet so only the beginning of it shows.  For this reason, don't set an inline width on it, or the stylesheet won't be able to override it.
				newtick.style.width = (ticksize*100) + "%";
			}
			
			
		}
		
		return(true);
	};

	// Internal functions:

	// 	moveSliderToRelativeXY - abstracts out "move thumb div to position of mouse event" logic so we can have the same code being called both by the thumb being dragged *and* the background being clicked.
	// Just to be helpful, it returns the mouse-event co-ordinates in a hash for debugging purposes
	// CLARIFICATION: the x,y coordinates it accepts and returns are the coordinates of the MIDDLE of the thumb slider
	this.moveSliderMiddleToRelativeXY = function(x, y, thumb, background)
	{	// this == Phoenix.Widgets.Slider
		if(!this.rendered)	// Relies on defined widget dimensions - useless before widget is rendered
			return(false);	

		thumb = thumb || this.thumb;
		background = background || this.background;
	
		if(!event) var event = window.event;

		// Snap x to nearest integer, and then shift it forwards so its position is 0->getSliderMaxX, instead of (-5)->(getSliderMaxX-5) 	
		x = parseInt(x);
		y = parseInt(y);
		
		// Set legal limits for the x position (basically, the width of the background div) 
		var lowestvalidx = 0;
//		alert("moveSliderMiddleToRelativeXY: " + background.offsetWidth);
		var highestvalidx = this.getSliderMaxX(background);
		
		// Bounds-check so the thumb doesn't slide off the ends of the scale
		if(x < lowestvalidx)
			x = lowestvalidx;
		else if(x > highestvalidx)
			x = highestvalidx;
			
		// Now convert 0->getSliderMaxX scale to (-5)->(getSliderMaxX-5) to position the actual div:
		 var actualdivxpos = x - this.getHalfThumbWidth(thumb);
		 
		// Check to see if we've changed the value at all, or if it's still the same as it was (decides whether we fire an onchange event or not)				
		if(thumb.offsetLeft != actualdivxpos)
			this.hasBeenDragged = true;
		
		thumb.style.left = actualdivxpos + "px";
		
		return({x:x, y:y});	// And return co-ordinates of the MIDDLE of the thumb 
	};
	
	// 	relativeXYToSliderValue - calculates the slider value (0-1) of an arbitrary x,y point (where x and y are co-ordinates relative to the slider background div)
	this.relativeXYToSliderValue = function(x, y, background)
	{
		if(!this.rendered)	// Relies on defined widget dimensions - useless before widget is rendered
			return(false);	

		background = background || this.background;
		
		var lowestvalidx = 0;	// leftmost co-ordinate of slider background div
		var highestvalidx = this.getSliderMaxX(background);
			
		// Bounds-check the scale (overruns are cropped to end of the scale)
		if(x < lowestvalidx)
			x = lowestvalidx;
		else if(x > highestvalidx)
			x = highestvalidx;

		var valueasfraction = (x / highestvalidx);
		
		var value = (valueasfraction * (this.maxValue - this.minValue)) +  this.minValue;
		
		//calculate the x position as a fraction of the total scale width
		return(value);
	};

	// 	sliderValueToRelativeXY - calculates the x/y coordinates of an arbitrary slider value (0-1)
	// x,y values returned range from 0-getSliderMaxX
	this.sliderValueToRelativeXY = function(value, background)
	{
		if(!this.rendered)	// Relies on defined widget dimensions - useless before widget is rendered
			return(false);	

		background = background || this.background;
		
		value = parseFloat(value);
		
		if(value > this.maxValue || value < this.minValue || isNaN(value))
			return(false);
		
		var valueasfraction = (value - this.minValue) / (this.maxValue - this.minValue);

		var slidermax = this.getSliderMaxX(background);
		
		var x = Math.round(valueasfraction * slidermax);

		return({x:x, y:0});
	};
	
	
	// getThumbPosition - returns the position of the thumb's central hotspot in pixels
	// CLARIFICATION: range returned is from 0->getSliderMaxX
	this.getThumbPosition = function(thumb)
	{
		if(!this.rendered)	// Relies on defined widget dimensions - useless before widget is rendered
			return(false);	

		thumb = thumb || this.thumb;
		return(thumb.offsetLeft + this.getHalfThumbWidth(thumb));
	};

	// 	getHalfThumbWidth - calculates half the width of the passed slider thumb.  Not really a complex function, but it's called from several different places in the code and where the thumb is styled to be an odd number of pixels wide it's essential that we do the calculation *exactly* the same way each time (rounding up or down consistently) 
	this.getHalfThumbWidth = function(thumb)
	{
		if(!this.rendered)	// Relies on defined widget dimensions - useless before widget is rendered
			return(false);	

		thumb = thumb || this.thumb;
		return(Math.round((thumb.offsetWidth/2)-0.25));	// Round thumbwidth DOWN to nearest integer (arbitrary, but must be the same for every thumb-width calculation to avoid inconsistency and off-by-one-pixel errors)
	};
	
	// getSliderMaxX - returns the width of the slider's "scale" in pixels (allowing one pixel for the thumb's central "hotspot", which must stay in the div at both ends)
	// This is basically background.offsetWidth-1
	this.getSliderMaxX = function(background)
	{
		if(!this.rendered)	// Relies on defined widget dimensions - useless before widget is rendered
			return(false);	

		background = background || this.background;
//		alert("Test: " + background.offsetWidth);
		//debugger
		if(background.offsetWidth > 0)
			return(background.offsetWidth-1);	// ensure slider "hot point" *within* slider background - not inside-left to outside-right
		else
			return(false);
	};


	// Attach event handlers
	
	this.thumb.onmousedown = function (event)
	{	// this == Phoenix.Widgets.Slider.thumb;
		// Fire any user-supplied event handlers, but let the event bubble up to the background element for actual handling 
		if(this.owningControl.onthumbmousedown)
			this.owningControl.onthumbmousedown(event);
	};
	
	
	this.background.onmousedown = function (event)
	{
		event = event || window.event;
		this.owningControl.mouseIsDown = true;
		
		// Need to get coords of thumb relative to background slider.  Can't just use event.offsetX/layerX because if someone clicks *on* the thumb slider these values will be relative to the thumb slider (ie, < 10), not to the slider background.
		var newcoords = Phoenix.Util.Mouse.getRelativePosition(event, this.owningControl.background);	// KNOWN BUGGY: If problems occur with the following function call, Phoenix.DOM.getAbsolutePosition() is likely misreporting the correct position of one or more elements due to one of a multitude of browser-specific bugs.  Try to hunt down what's causing it and insert a special-case into the P.D.gAP function if possible.
		var newvalue = this.owningControl.relativeXYToSliderValue(newcoords.x, newcoords.y);	// slider value that's represented by the x,y co-ordinates of the mouseclick
		this.owningControl.setValue(newvalue);
		//this.owningControl.moveSliderMiddleToRelativeXY(newcoords.x, newcoords.y, this.owningControl.thumb, this.owningControl.background);	// Move the slider to the coordinates of the mouse event
		

		this.onmousemove = function(event)
		{	// this == Phoenix.Widgets.Slider.background
		
			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...
			{
				requireNamespace("Phoenix.Util.Mouse");

				var newcoords = Phoenix.Util.Mouse.getRelativePosition(event, this.owningControl.background);	// Get desired coords of the MIDDLE of the thumb
				var newvalue = this.owningControl.relativeXYToSliderValue(newcoords.x, newcoords.y);	// slider value that's represented by the x,y co-ordinates of the mouseclick
				this.owningControl.setValue(newvalue);
				//this.owningControl.moveSliderMiddleToRelativeXY(newcoords.x, newcoords.y, this.owningControl.thumb, this.owningControl.background);	// Move the slider to the coordinates of the mouse event
			}
			
			// 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;
							
		// 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.onmouseout = function(event)
	{
		if(this.owningControl.mouseIsDown)
		{
			var backgroundbox = Phoenix.DOM.getAbsolutePosition(this);
			backgroundbox.width = this.offsetWidth;
			backgroundbox.height = this.offsetHeight;

			var eventpoint = Phoenix.Util.Mouse.getAbsolutePosition(event);
			
			if(eventpoint.x < backgroundbox.x || eventpoint.x > backgroundbox.x+backgroundbox.width ||
				eventpoint.y < backgroundbox.y || eventpoint.y > backgroundbox.y+backgroundbox.height)
			{
				this.owningControl.mouseIsDown = false;
				this.owningControl.background.onmouseup(event);
			}
									
		}
	}
	
	this.background.onchange = function(event)
	{
		if(this.owningControl.onchange)
			this.owningControl.onchange(event);
	};
	
	
	
	
	// Member functions (methods)
	
	// getValue()
	// Returns a value between this.minValue and this.maxValue which represents the value the MIDDLE of the slider is positioned to (where minValue is the very left-hand end of the slider and maxValue is the very right-hand end)
	this.getValue = function (thumb, background)
	{
		thumb = thumb || this.thumb;
		background = background || this.background;

		return(thumb.prerenderedValue);
	};
	
	// setValue(newval, thumb, background)
	// Sets the value of the slider.  newval must be between this.minValue and this.maxValue

	this.setValue = function (newval, thumb, background)
	{
		thumb = thumb || this.thumb;
		background = background || this.background;

		// 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);
		
		// If the new value passes the sanity check, update the thumb's internal state to reflect the new value
		thumb.prerenderedValue = newval;
		
		if(!this.rendered)	// If the thumb hasn't been rendered yet then this is all we have to do 
			return(true);
		
		// Otherwise, we need to calculate the new position of the thumb and move it there.
		
		// Adjust newval if necessary so it's compressed from the range this.minValue-this.maxValue to the range 0-1
//		newval -= this.minValue;
//		newval /= (this.maxValue-this.minValue);
		
		var newcoords = this.sliderValueToRelativeXY(newval);
		this.moveSliderMiddleToRelativeXY(newcoords.x, newcoords.y, thumb);
		
		// 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(true);
	};
	
	
};