/** 
 * @file llmultisldr.cpp
 * @brief LLMultiSlider base class
 *
 * $LicenseInfo:firstyear=2007&license=viewergpl$
 * 
 * Copyright (c) 2007-2009, Linden Research, Inc.
 * 
 * Second Life Viewer Source Code
 * The source code in this file ("Source Code") is provided by Linden Lab
 * to you under the terms of the GNU General Public License, version 2.0
 * ("GPL"), unless you have obtained a separate licensing agreement
 * ("Other License"), formally executed by you and Linden Lab.  Terms of
 * the GPL can be found in doc/GPL-license.txt in this distribution, or
 * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
 * 
 * There are special exceptions to the terms and conditions of the GPL as
 * it is applied to this Source Code. View the full text of the exception
 * in the file doc/FLOSS-exception.txt in this software distribution, or
 * online at
 * http://secondlifegrid.net/programs/open_source/licensing/flossexception
 * 
 * By copying, modifying or distributing this software, you acknowledge
 * that you have read and understood your obligations described above,
 * and agree to abide by those obligations.
 * 
 * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
 * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
 * COMPLETENESS OR PERFORMANCE.
 * $/LicenseInfo$
 */

#include "linden_common.h"

#include "llmultislider.h"
#include "llui.h"

#include "llgl.h"
#include "llwindow.h"
#include "llfocusmgr.h"
#include "llkeyboard.h"			// for the MASK constants
#include "llcontrol.h"
#include "lluiimage.h"

#include <sstream>

static LLRegisterWidget<LLMultiSlider> r("multi_slider_bar");

const S32 MULTI_THUMB_WIDTH = 8;
const S32 MULTI_TRACK_HEIGHT = 6;
const F32 FLOAT_THRESHOLD = 0.00001f;
const S32 EXTRA_TRIANGLE_WIDTH = 2;
const S32 EXTRA_TRIANGLE_HEIGHT = -2;

S32 LLMultiSlider::mNameCounter = 0;

LLMultiSlider::LLMultiSlider( 
	const std::string& name,
	const LLRect& rect,
	void (*on_commit_callback)(LLUICtrl* ctrl, void* userdata),
	void* callback_userdata,
	F32 initial_value,
	F32 min_value,
	F32 max_value,
	F32 increment,
	S32 max_sliders,
	BOOL allow_overlap,
	BOOL draw_track,
	BOOL use_triangle,
	const std::string& control_name)
	:
	LLUICtrl( name, rect, TRUE,	on_commit_callback, callback_userdata, 
		FOLLOWS_LEFT | FOLLOWS_TOP),

	mInitialValue( initial_value ),
	mMinValue( min_value ),
	mMaxValue( max_value ),
	mIncrement( increment ),
	mMaxNumSliders(max_sliders),
	mAllowOverlap(allow_overlap),
	mDrawTrack(draw_track),
	mUseTriangle(use_triangle),
	mMouseOffset( 0 ),
	mDragStartThumbRect( 0, getRect().getHeight(), MULTI_THUMB_WIDTH, 0 ),
	mTrackColor(		LLUI::sColorsGroup->getColor( "MultiSliderTrackColor" ) ),
	mThumbOutlineColor(	LLUI::sColorsGroup->getColor( "MultiSliderThumbOutlineColor" ) ),
	mThumbCenterColor(	LLUI::sColorsGroup->getColor( "MultiSliderThumbCenterColor" ) ),
	mThumbCenterSelectedColor(	LLUI::sColorsGroup->getColor( "MultiSliderThumbCenterSelectedColor" ) ),
	mDisabledThumbColor(LLUI::sColorsGroup->getColor( "MultiSliderDisabledThumbColor" ) ),
	mTriangleColor(LLUI::sColorsGroup->getColor( "MultiSliderTriangleColor" ) ),
	mMouseDownSignal( NULL ),
	mMouseUpSignal( NULL ),
	mThumbWidth(MULTI_THUMB_WIDTH)
{
	mValue.emptyMap();
	mCurSlider = LLStringUtil::null;

	// properly handle setting the starting thumb rect
	// do it this way to handle both the operating-on-settings
	// and standalone ways of using this
	setControlName(control_name, NULL);
	setValue(getValue());
}

LLMultiSlider::~LLMultiSlider()
{
	delete mMouseDownSignal;
	delete mMouseUpSignal;
}


void LLMultiSlider::setSliderValue(const std::string& name, F32 value, BOOL from_event)
{
	// exit if not there
	if(!mValue.has(name)) {
		return;
	}

	value = llclamp( value, mMinValue, mMaxValue );

	// Round to nearest increment (bias towards rounding down)
	value -= mMinValue;
	value += mIncrement/2.0001f;
	value -= fmod(value, mIncrement);
	F32 newValue = mMinValue + value;

	// now, make sure no overlap
	// if we want that
	if(!mAllowOverlap) {
		bool hit = false;

		// look at the current spot
		// and see if anything is there
		LLSD::map_iterator mIt = mValue.beginMap();
		for(;mIt != mValue.endMap(); mIt++) {
			
			F32 testVal = (F32)mIt->second.asReal() - newValue;
			if(testVal > -FLOAT_THRESHOLD && testVal < FLOAT_THRESHOLD &&
				mIt->first != name) {
				hit = true;
				break;
			}
		}

		// if none found, stop
		if(hit) {
			return;
		}
	}
	

	// now set it in the map
	mValue[name] = newValue;

	// set the control if it's the current slider and not from an event
	if (!from_event && name == mCurSlider)
	{
		setControlValue(mValue);
	}
	
	F32 t = (newValue - mMinValue) / (mMaxValue - mMinValue);

	S32 left_edge = mThumbWidth/2;
	S32 right_edge = getRect().getWidth() - (mThumbWidth/2);

	S32 x = left_edge + S32( t * (right_edge - left_edge) );
	mThumbRects[name].mLeft = x - (mThumbWidth/2);
	mThumbRects[name].mRight = x + (mThumbWidth/2);
}

void LLMultiSlider::setValue(const LLSD& value)
{
	// only do if it's a map
	if(value.isMap()) {
		
		// add each value... the first in the map becomes the current
		LLSD::map_const_iterator mIt = value.beginMap();
		mCurSlider = mIt->first;

		for(; mIt != value.endMap(); mIt++) {
			setSliderValue(mIt->first, (F32)mIt->second.asReal(), TRUE);
		}
	}
}

F32 LLMultiSlider::getSliderValue(const std::string& name) const
{
	return (F32)mValue[name].asReal();
}

void LLMultiSlider::setCurSlider(const std::string& name)
{
	if(mValue.has(name)) {
		mCurSlider = name;
	}
}

const std::string& LLMultiSlider::addSlider()
{
	return addSlider(mInitialValue);
}

const std::string& LLMultiSlider::addSlider(F32 val)
{
	std::stringstream newName;
	F32 initVal = val;

	if(mValue.size() >= mMaxNumSliders) {
		return LLStringUtil::null;
	}

	// create a new name
	newName << "sldr" << mNameCounter;
	mNameCounter++;

	bool foundOne = findUnusedValue(initVal);
	if(!foundOne) {
		return LLStringUtil::null;
	}

	// add a new thumb rect
	mThumbRects[newName.str()] = LLRect( 0, getRect().getHeight(), mThumbWidth, 0 );

	// add the value and set the current slider to this one
	mValue.insert(newName.str(), initVal);
	mCurSlider = newName.str();

	// move the slider
	setSliderValue(mCurSlider, initVal, TRUE);

	return mCurSlider;
}

void LLMultiSlider::addSlider(F32 val, const std::string& name)
{
	F32 initVal = val;

	if(mValue.size() >= mMaxNumSliders) {
		return;
	}

	bool foundOne = findUnusedValue(initVal);
	if(!foundOne) {
		return;
	}

	// add a new thumb rect
	mThumbRects[name] = LLRect( 0, getRect().getHeight(), mThumbWidth, 0 );

	// add the value and set the current slider to this one
	mValue.insert(name, initVal);
	mCurSlider = name;

	// move the slider
	setSliderValue(mCurSlider, initVal, TRUE);
}

bool LLMultiSlider::findUnusedValue(F32& initVal)
{
	bool firstTry = true;

	// find the first open slot starting with
	// the initial value
	while(true) {
		
		bool hit = false;

		// look at the current spot
		// and see if anything is there
		LLSD::map_iterator mIt = mValue.beginMap();
		for(;mIt != mValue.endMap(); mIt++) {
			
			F32 testVal = (F32)mIt->second.asReal() - initVal;
			if(testVal > -FLOAT_THRESHOLD && testVal < FLOAT_THRESHOLD) {
				hit = true;
				break;
			}
		}

		// if we found one
		if(!hit) {
			break;
		}

		// increment and wrap if need be
		initVal += mIncrement;
		if(initVal > mMaxValue) {
			initVal = mMinValue;
		}

		// stop if it's filled
		if(initVal == mInitialValue && !firstTry) {
			llwarns << "Whoa! Too many multi slider elements to add one to" << llendl;
			return false;
		}

		firstTry = false;
		continue;
	}

	return true;
}


void LLMultiSlider::deleteSlider(const std::string& name)
{
	// can't delete last slider
	if(mValue.size() <= 0) {
		return;
	}

	// get rid of value from mValue and its thumb rect
	mValue.erase(name);
	mThumbRects.erase(name);

	// set to the last created
	if(mValue.size() > 0) {
		std::map<std::string, LLRect>::iterator mIt = mThumbRects.end();
		mIt--;
		mCurSlider = mIt->first;
	}
}

void LLMultiSlider::clear()
{
	while(mThumbRects.size() > 0) {
		deleteCurSlider();
	}

	LLUICtrl::clear();
}

BOOL LLMultiSlider::handleHover(S32 x, S32 y, MASK mask)
{
	if( gFocusMgr.getMouseCapture() == this )
	{
		S32 left_edge = mThumbWidth/2;
		S32 right_edge = getRect().getWidth() - (mThumbWidth/2);

		x += mMouseOffset;
		x = llclamp( x, left_edge, right_edge );

		F32 t = F32(x - left_edge) / (right_edge - left_edge);
		setCurSliderValue(t * (mMaxValue - mMinValue) + mMinValue );
		onCommit();

		getWindow()->setCursor(UI_CURSOR_ARROW);
		lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << " (active)" << llendl;		
	}
	else
	{
		getWindow()->setCursor(UI_CURSOR_ARROW);
		lldebugst(LLERR_USER_INPUT) << "hover handled by " << getName() << " (inactive)" << llendl;		
	}
	return TRUE;
}

BOOL LLMultiSlider::handleMouseUp(S32 x, S32 y, MASK mask)
{
	BOOL handled = FALSE;

	if( gFocusMgr.getMouseCapture() == this )
	{
		gFocusMgr.setMouseCapture( NULL );

		if (mMouseUpSignal)
			(*mMouseUpSignal)( this, LLSD() );

		handled = TRUE;
		make_ui_sound("UISndClickRelease");
	}
	else
	{
		handled = TRUE;
	}

	return handled;
}

BOOL LLMultiSlider::handleMouseDown(S32 x, S32 y, MASK mask)
{
	// only do sticky-focus on non-chrome widgets
	if (!getIsChrome())
	{
		setFocus(TRUE);
	}
	if (mMouseDownSignal)
		(*mMouseDownSignal)( this, LLSD() );

	if (MASK_CONTROL & mask) // if CTRL is modifying
	{
		setCurSliderValue(mInitialValue);
		onCommit();
	}
	else
	{
		// scroll through thumbs to see if we have a new one selected and select that one
		std::map<std::string, LLRect>::iterator mIt = mThumbRects.begin();
		for(; mIt != mThumbRects.end(); mIt++) {
			
			// check if inside.  If so, set current slider and continue
			if(mIt->second.pointInRect(x,y)) {
				mCurSlider = mIt->first;
				break;
			}
		}

		// Find the offset of the actual mouse location from the center of the thumb.
		if (mThumbRects[mCurSlider].pointInRect(x,y))
		{
			mMouseOffset = (mThumbRects[mCurSlider].mLeft + mThumbWidth/2) - x;
		}
		else
		{
			mMouseOffset = 0;
		}

		// Start dragging the thumb
		// No handler needed for focus lost since this class has no state that depends on it.
		gFocusMgr.setMouseCapture( this );
		mDragStartThumbRect = mThumbRects[mCurSlider];				
	}
	make_ui_sound("UISndClick");

	return TRUE;
}

BOOL	LLMultiSlider::handleKeyHere(KEY key, MASK mask)
{
	BOOL handled = FALSE;
	switch(key)
	{
	case KEY_UP:
	case KEY_DOWN:
		// eat up and down keys to be consistent
		handled = TRUE;
		break;
	case KEY_LEFT:
		setCurSliderValue(getCurSliderValue() - getIncrement());
		onCommit();
		handled = TRUE;
		break;
	case KEY_RIGHT:
		setCurSliderValue(getCurSliderValue() + getIncrement());
		onCommit();
		handled = TRUE;
		break;
	default:
		break;
	}
	return handled;
}

void LLMultiSlider::draw()
{
	LLColor4 curThumbColor;

	std::map<std::string, LLRect>::iterator mIt;
	std::map<std::string, LLRect>::iterator curSldrIt;

	// Draw background and thumb.

	// drawing solids requires texturing be disabled
	gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE);

	LLRect rect(mDragStartThumbRect);

	F32 opacity = getEnabled() ? 1.f : 0.3f;

	// Track
	LLUIImagePtr thumb_imagep = LLUI::getUIImage("rounded_square.tga");

	S32 height_offset = (getRect().getHeight() - MULTI_TRACK_HEIGHT) / 2;
	LLRect track_rect(0, getRect().getHeight() - height_offset, getRect().getWidth(), height_offset );


	if(mDrawTrack)
	{
		track_rect.stretch(-1);
		thumb_imagep->draw(track_rect, mTrackColor % opacity);
	}

	// if we're supposed to use a drawn triangle
	// simple gl call for the triangle
	if(mUseTriangle) {

		for(mIt = mThumbRects.begin(); mIt != mThumbRects.end(); mIt++) {

			gl_triangle_2d(
				mIt->second.mLeft - EXTRA_TRIANGLE_WIDTH, 
				mIt->second.mTop + EXTRA_TRIANGLE_HEIGHT,
				mIt->second.mRight + EXTRA_TRIANGLE_WIDTH, 
				mIt->second.mTop + EXTRA_TRIANGLE_HEIGHT,
				mIt->second.mLeft + mIt->second.getWidth() / 2, 
				mIt->second.mBottom - EXTRA_TRIANGLE_HEIGHT,
				mTriangleColor, TRUE);
		}
	}
	else if (!thumb_imagep)
	{
		// draw all the thumbs
		curSldrIt = mThumbRects.end();
		for(mIt = mThumbRects.begin(); mIt != mThumbRects.end(); mIt++) {
			
			// choose the color
			curThumbColor = mThumbCenterColor;
			if(mIt->first == mCurSlider) {
				
				curSldrIt = mIt;
				continue;
				//curThumbColor = mThumbCenterSelectedColor;
			}

			// the draw command
			gl_rect_2d(mIt->second, curThumbColor, TRUE);
		}

		// now draw the current slider
		if(curSldrIt != mThumbRects.end()) {
			gl_rect_2d(curSldrIt->second, mThumbCenterSelectedColor, TRUE);
		}

		// and draw the drag start
		if (gFocusMgr.getMouseCapture() == this)
		{
			gl_rect_2d(mDragStartThumbRect, mThumbCenterColor % opacity, FALSE);
		}
	}
	else if( gFocusMgr.getMouseCapture() == this )
	{
		// draw drag start
		thumb_imagep->drawSolid(mDragStartThumbRect, mThumbCenterColor % 0.3f);

		// draw the highlight
		if (hasFocus())
		{
			thumb_imagep->drawBorder(mThumbRects[mCurSlider], gFocusMgr.getFocusColor(), gFocusMgr.getFocusFlashWidth());
		}

		// draw the thumbs
		curSldrIt = mThumbRects.end();
		for(mIt = mThumbRects.begin(); mIt != mThumbRects.end(); mIt++) 
		{
			// choose the color
			curThumbColor = mThumbCenterColor;
			if(mIt->first == mCurSlider) 
			{
				// don't draw now, draw last
				curSldrIt = mIt;
				continue;				
			}
			
			// the draw command
			thumb_imagep->drawSolid(mIt->second, curThumbColor);
		}
		
		// draw cur slider last
		if(curSldrIt != mThumbRects.end()) 
		{
			thumb_imagep->drawSolid(curSldrIt->second, mThumbCenterSelectedColor);
		}
		
	}
	else
	{ 
		// draw highlight
		if (hasFocus())
		{
			thumb_imagep->drawBorder(mThumbRects[mCurSlider], gFocusMgr.getFocusColor(), gFocusMgr.getFocusFlashWidth());
		}

		// draw thumbs
		curSldrIt = mThumbRects.end();
		for(mIt = mThumbRects.begin(); mIt != mThumbRects.end(); mIt++) 
		{
			
			// choose the color
			curThumbColor = mThumbCenterColor;
			if(mIt->first == mCurSlider) 
			{
				curSldrIt = mIt;
				continue;
				//curThumbColor = mThumbCenterSelectedColor;
			}				
			
			thumb_imagep->drawSolid(mIt->second, curThumbColor % opacity);
		}

		if(curSldrIt != mThumbRects.end()) 
		{
			thumb_imagep->drawSolid(curSldrIt->second, mThumbCenterSelectedColor % opacity);
		}
	}

	LLUICtrl::draw();
}

boost::signals2::connection LLMultiSlider::setMouseDownCallback( const commit_signal_t::slot_type& cb ) 
{ 
	if (!mMouseDownSignal) mMouseDownSignal = new commit_signal_t();
	return mMouseDownSignal->connect(cb); 
}

boost::signals2::connection LLMultiSlider::setMouseUpCallback(	const commit_signal_t::slot_type& cb )   
{ 
	if (!mMouseUpSignal) mMouseUpSignal = new commit_signal_t();
	return mMouseUpSignal->connect(cb); 
}

// virtual
LLXMLNodePtr LLMultiSlider::getXML(bool save_children) const
{
	LLXMLNodePtr node = LLUICtrl::getXML();

	node->setName(LL_MULTI_SLIDER_TAG);

	node->createChild("initial_val", TRUE)->setFloatValue(getInitialValue());
	node->createChild("min_val", TRUE)->setFloatValue(getMinValue());
	node->createChild("max_val", TRUE)->setFloatValue(getMaxValue());
	node->createChild("increment", TRUE)->setFloatValue(getIncrement());

	return node;
}


//static
LLView* LLMultiSlider::fromXML(LLXMLNodePtr node, LLView *parent, LLUICtrlFactory *factory)
{
	std::string name("multi_slider_bar");
	node->getAttributeString("name", name);

	LLRect rect;
	createRect(node, rect, parent, LLRect());

	F32 initial_value = 0.f;
	node->getAttributeF32("initial_val", initial_value);

	F32 min_value = 0.f;
	node->getAttributeF32("min_val", min_value);

	F32 max_value = 1.f; 
	node->getAttributeF32("max_val", max_value);

	F32 increment = 0.1f;
	node->getAttributeF32("increment", increment);

	S32 max_sliders = 1;
	node->getAttributeS32("max_sliders", max_sliders);

	BOOL allow_overlap = FALSE;
	node->getAttributeBOOL("allow_overlap", allow_overlap);

	BOOL draw_track = TRUE;
	node->getAttributeBOOL("draw_track", draw_track);

	BOOL use_triangle = FALSE;
	node->getAttributeBOOL("use_triangle", use_triangle);

	LLMultiSlider* multiSlider = new LLMultiSlider(name,
							rect,
							NULL,
							NULL,
							initial_value,
							min_value,
							max_value,
							increment,
							max_sliders,
							allow_overlap,
							draw_track,
							use_triangle);

	multiSlider->initFromXML(node, parent);

	return multiSlider;
}
