/** 
 * @file llspinctrl.cpp
 * @brief LLSpinCtrl base class
 *
 * $LicenseInfo:firstyear=2001&license=viewergpl$
 * 
 * Copyright (c) 2001-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 "llspinctrl.h"

#include "llgl.h"
#include "llui.h"
#include "lluiconstants.h"

#include "llstring.h"
#include "llfontgl.h"
#include "lllineeditor.h"
#include "llbutton.h"
#include "lltextbox.h"
#include "llkeyboard.h"
#include "llmath.h"
#include "llcontrol.h"
#include "llfocusmgr.h"
#include "llresmgr.h"

const U32 MAX_STRING_LENGTH = 32;

static LLRegisterWidget<LLSpinCtrl> r2("spinner");
 
LLSpinCtrl::LLSpinCtrl(	const std::string& name, const LLRect& rect, const std::string& label, const LLFontGL* font,
	void (*commit_callback)(LLUICtrl*, void*),
	void* callback_user_data,
	F32 initial_value, F32 min_value, F32 max_value, F32 increment,
	const std::string& control_name,
	S32 label_width)
	:
	LLUICtrl(name, rect, TRUE, commit_callback, callback_user_data, FOLLOWS_LEFT | FOLLOWS_TOP ),
	mValue( initial_value ),
	mInitialValue( initial_value ),
	mMaxValue( max_value ),
	mMinValue( min_value ),
	mIncrement( increment ),
	mPrecision( 3 ),
	mLabelBox( NULL ),
	mTextEnabledColor( LLUI::sColorsGroup->getColor( "LabelTextColor" ) ),
	mTextDisabledColor( LLUI::sColorsGroup->getColor( "LabelDisabledColor" ) ),
	mbHasBeenSet( FALSE )
{
	S32 top = getRect().getHeight();
	S32 bottom = top - 2 * SPINCTRL_BTN_HEIGHT;
	S32 centered_top = top;
	S32 centered_bottom = bottom;
	S32 btn_left = 0;

	// Label
	if( !label.empty() )
	{
		LLRect label_rect( 0, centered_top, label_width, centered_bottom );
		mLabelBox = new LLTextBox( std::string("SpinCtrl Label"), label_rect, label, font );
		addChild(mLabelBox);

		btn_left += label_rect.mRight + SPINCTRL_SPACING;
	}

	S32 btn_right = btn_left + SPINCTRL_BTN_WIDTH;
	
	// Spin buttons
	LLRect up_rect( btn_left, top, btn_right, top - SPINCTRL_BTN_HEIGHT );
	std::string out_id = "UIImgBtnSpinUpOutUUID";
	std::string in_id = "UIImgBtnSpinUpInUUID";
	mUpBtn = new LLButton(std::string("SpinCtrl Up"), up_rect,
								   out_id,
								   in_id,
								   LLStringUtil::null,
								   &LLSpinCtrl::onUpBtn, this, LLFontGL::getFontSansSerif() );
	mUpBtn->setFollowsLeft();
	mUpBtn->setFollowsBottom();
	mUpBtn->setHeldDownCallback(boost::bind(&LLSpinCtrl::onUpBtn,this));
	mUpBtn->setTabStop(FALSE);
	addChild(mUpBtn);

	LLRect down_rect( btn_left, top - SPINCTRL_BTN_HEIGHT, btn_right, bottom );
	out_id = "UIImgBtnSpinDownOutUUID";
	in_id = "UIImgBtnSpinDownInUUID";
	mDownBtn = new LLButton(std::string("SpinCtrl Down"), down_rect,
							out_id,
							in_id,
							LLStringUtil::null,
							&LLSpinCtrl::onDownBtn, this, LLFontGL::getFontSansSerif() );
	mDownBtn->setFollowsLeft();
	mDownBtn->setFollowsBottom();
	mDownBtn->setHeldDownCallback(boost::bind(&LLSpinCtrl::onDownBtn,this));
	mDownBtn->setTabStop(FALSE);
	addChild(mDownBtn);

	LLRect editor_rect( btn_right + 1, centered_top, getRect().getWidth(), centered_bottom );
	mEditor = new LLLineEditor( std::string("SpinCtrl Editor"), editor_rect, LLStringUtil::null, font,
								MAX_STRING_LENGTH,
								&LLSpinCtrl::onEditorCommit, NULL, NULL, this,
								&LLLineEditor::prevalidateASCII );
	mEditor->setFollowsLeft();
	mEditor->setFollowsBottom();
	mEditor->setFocusReceivedCallback( boost::bind(&LLSpinCtrl::onFocusReceived, this) );
	//RN: this seems to be a BAD IDEA, as it makes the editor behavior different when it has focus
	// than when it doesn't.  Instead, if you always have to double click to select all the text, 
	// it's easier to understand
	//mEditor->setSelectAllonFocusReceived(TRUE);
	mEditor->setIgnoreTab(TRUE);
	mEditor->setSelectAllonCommit(FALSE);
	addChild(mEditor);

	updateEditor();
	setUseBoundingRect( TRUE );
}


F32 clamp_precision(F32 value, S32 decimal_precision)
{
	// pow() isn't perfect
	
	F64 clamped_value = value;
	for (S32 i = 0; i < decimal_precision; i++)
		clamped_value *= 10.0;

	clamped_value = llround((F32)clamped_value);

	for (S32 i = 0; i < decimal_precision; i++)
		clamped_value /= 10.0;
	
	return (F32)clamped_value;
}


F32 get_increment(F32 inc, S32 decimal_precision) //CF: finetune increments
{
	if(gKeyboard->getKeyDown(KEY_ALT))
		inc = inc * 10.f;
	else if(gKeyboard->getKeyDown(KEY_CONTROL)) {
		if (llround(inc * 1000.f) == 25) // 0.025 gets 0.05 here
			inc = inc * 0.2f;
		else
			inc = inc * 0.1f;
	}
	else if(gKeyboard->getKeyDown(KEY_SHIFT)) {
		if (decimal_precision == 2 && llround(inc) == 1) // for rotations, finest step is 0.05
			inc = inc * 0.05f;
		else
			inc = inc * 0.01f;
	}

	if (decimal_precision == 1 && inc < 0.1f) // clamp inc to precision
		inc = 0.1f;
	else if (decimal_precision == 2 && inc < 0.01f)
		inc = 0.01f;
	else if (decimal_precision == 3 && inc < 0.001f)
		inc = 0.001f;

	return inc;
}


// static
void LLSpinCtrl::onUpBtn( void *userdata )
{
	LLSpinCtrl* self = (LLSpinCtrl*) userdata;
	if( self->getEnabled() )
	{
		// use getValue()/setValue() to force reload from/to control
		F32 val = (F32)self->getValue().asReal() + get_increment(self->mIncrement, self->mPrecision);
		val = clamp_precision(val, self->mPrecision);
		val = llmin( val, self->mMaxValue );
		
		F32 saved_val = (F32)self->getValue().asReal();
		self->setValue(val);
		if( (self->mValidateCallback 	&& !self->mValidateCallback( self, self->mCallbackUserData ) ) ||
			(self->mValidateSignal		&& !(*(self->mValidateSignal))( self, val ) ))
		{
			self->setValue( saved_val );
			self->reportInvalidData();
			self->updateEditor();
			return;
		}

		self->updateEditor();
		self->onCommit();
	}
}

// static
void LLSpinCtrl::onDownBtn( void *userdata )
{
	LLSpinCtrl* self = (LLSpinCtrl*) userdata;

	if( self->getEnabled() )
	{
		F32 val = (F32)self->getValue().asReal() - get_increment(self->mIncrement, self->mPrecision);
		val = clamp_precision(val, self->mPrecision);
		val = llmax( val, self->mMinValue );


		if (val < self->mMinValue) val = self->mMinValue;
		if (val > self->mMaxValue) val = self->mMaxValue;

		F32 saved_val = (F32)self->getValue().asReal();
		self->setValue(val);
		if( (self->mValidateCallback 	&& !self->mValidateCallback( self, self->mCallbackUserData ) ) ||
			(self->mValidateSignal		&& !(*(self->mValidateSignal))( self, val ) ))
		{
			self->setValue( saved_val );
			self->reportInvalidData();
			self->updateEditor();
			return;
		}
		
		self->updateEditor();
		self->onCommit();
	}
}

void LLSpinCtrl::setValue(const LLSD& value )
{
	F32 v = (F32)value.asReal();
	if (mValue != v || !mbHasBeenSet)
	{
		mbHasBeenSet = TRUE;
		mValue = v;
		
		if (!mEditor->hasFocus())
		{
			updateEditor();
		}
	}
}

//no matter if Editor has the focus, update the value
void LLSpinCtrl::forceSetValue(const LLSD& value )
{
	F32 v = (F32)value.asReal();
	if (mValue != v || !mbHasBeenSet)
	{
		mbHasBeenSet = TRUE;
		mValue = v;
		
		updateEditor();
	}
}

void LLSpinCtrl::clear()
{
	setValue(mMinValue);
	mEditor->clear();
	mbHasBeenSet = FALSE;
}



void LLSpinCtrl::updateEditor()
{
	LLLocale locale(LLLocale::USER_LOCALE);

	// Don't display very small negative values as -0.000
	F32 displayed_value = clamp_precision((F32)getValue().asReal(), mPrecision);

//	if( S32( displayed_value * pow( 10, mPrecision ) ) == 0 )
//	{
//		displayed_value = 0.f;
//	}

	std::string format = llformat("%%.%df", mPrecision);
	std::string text = llformat(format.c_str(), displayed_value);
	mEditor->setText( text );
}

void LLSpinCtrl::onEditorCommit( LLUICtrl* caller, void *userdata )
{
	BOOL success = FALSE;
	
	LLSpinCtrl* self = (LLSpinCtrl*) userdata;
	llassert( caller == self->mEditor );

	if( self->mEditor->evaluateFloat() )
	{
		std::string text = self->mEditor->getText();
		
		LLLocale locale(LLLocale::USER_LOCALE);
		F32 val = (F32) atof(text.c_str());

		if (val < self->mMinValue) val = self->mMinValue;
		if (val > self->mMaxValue) val = self->mMaxValue;

		F32 saved_val = self->mValue;
		self->mValue = val;
			
		if(	(!self->mValidateCallback	|| self->mValidateCallback( self, self->mCallbackUserData )) &&
			(!self->mValidateSignal		|| (*(self->mValidateSignal))(self, val)))
		{
			success = TRUE;
			self->onCommit();
		}
		else
		{
			self->mValue = saved_val;
		}
	}

	if( success )
	{
		self->updateEditor();
	}
	else
	{
		self->reportInvalidData();		
	}
}


void LLSpinCtrl::forceEditorCommit()
{
	onEditorCommit(mEditor, this);
}


void LLSpinCtrl::setFocus(BOOL b)
{
	LLUICtrl::setFocus( b );
	mEditor->setFocus( b );
}

void LLSpinCtrl::setEnabled(BOOL b)
{
	LLView::setEnabled( b );
	mEditor->setEnabled( b );
}


void LLSpinCtrl::setTentative(BOOL b)
{
	mEditor->setTentative(b);
	LLUICtrl::setTentative(b);
}


BOOL LLSpinCtrl::isMouseHeldDown() const
{
	return 
		mDownBtn->hasMouseCapture()
		|| mUpBtn->hasMouseCapture();
}

void LLSpinCtrl::onCommit()
{
	setTentative(FALSE);
	setControlValue(mValue);
	LLUICtrl::onCommit();
}


void LLSpinCtrl::setPrecision(S32 precision)
{
	if (precision < 0 || precision > 10)
	{
		llerrs << "LLSpinCtrl::setPrecision - precision out of range" << llendl;
		return;
	}

	mPrecision = precision;
	updateEditor();
}

void LLSpinCtrl::setLabel(const LLStringExplicit& label)
{
	if (mLabelBox)
	{
		mLabelBox->setText(label);
	}
	else
	{
		llwarns << "Attempting to set label on LLSpinCtrl constructed without one " << getName() << llendl;
	}
}

BOOL LLSpinCtrl::setLabelArg( const std::string& key, const LLStringExplicit& text )
{
	if (mLabelBox)
	{
		BOOL res = mLabelBox->setTextArg(key, text);
		reshape(getRect().getWidth(), getRect().getHeight(), FALSE);
		return res;
	}
	return FALSE;
}


void LLSpinCtrl::setAllowEdit(BOOL allow_edit)
{
	mEditor->setEnabled(allow_edit);
	mAllowEdit = allow_edit;
}

void LLSpinCtrl::onTabInto()
{
	mEditor->onTabInto(); 
}


void LLSpinCtrl::reportInvalidData()
{
	make_ui_sound("UISndBadKeystroke");
}

void LLSpinCtrl::draw()
{
	if( mLabelBox )
	{
		mLabelBox->setColor( getEnabled() ? mTextEnabledColor : mTextDisabledColor );
	}
	LLUICtrl::draw();
}


BOOL LLSpinCtrl::handleScrollWheel(S32 x, S32 y, S32 clicks)
{
	if( clicks > 0 )
	{
		while( clicks-- )
		{
			LLSpinCtrl::onDownBtn(this);
		}
	}
	else
	while( clicks++ )
	{
		LLSpinCtrl::onUpBtn(this);
	}

	return TRUE;
}

BOOL LLSpinCtrl::handleKeyHere(KEY key, MASK mask)
{
	if (mEditor->hasFocus())
	{
		if (key == KEY_ESCAPE && mask == MASK_NONE)
		{
			// text editors don't support revert normally (due to user confusion)
			// but not allowing revert on a spinner seems dangerous
			updateEditor();
			mEditor->setFocus(FALSE);
			return TRUE;
		}
		if(key == KEY_UP)
		{
			LLSpinCtrl::onUpBtn(this);
			return TRUE;
		}
		if(key == KEY_DOWN)
		{
			LLSpinCtrl::onDownBtn(this);
			return TRUE;
		}
		if(key == KEY_RETURN)
		{
			forceEditorCommit();
			return TRUE;
		}
	}
	return FALSE;
}

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

	node->setName(LL_SPIN_CTRL_TAG);

	node->createChild("decimal_digits", TRUE)->setIntValue(mPrecision);

	if (mLabelBox)
	{
		node->createChild("label", TRUE)->setStringValue(mLabelBox->getText());

		node->createChild("label_width", TRUE)->setIntValue(mLabelBox->getRect().getWidth());
	}

	node->createChild("initial_val", TRUE)->setFloatValue(mInitialValue);

	node->createChild("min_val", TRUE)->setFloatValue(mMinValue);

	node->createChild("max_val", TRUE)->setFloatValue(mMaxValue);

	node->createChild("increment", TRUE)->setFloatValue(mIncrement);
	
	addColorXML(node, mTextEnabledColor, "text_enabled_color", "LabelTextColor");
	addColorXML(node, mTextDisabledColor, "text_disabled_color", "LabelDisabledColor");

	return node;
}

LLView* LLSpinCtrl::fromXML(LLXMLNodePtr node, LLView *parent, LLUICtrlFactory *factory)
{
	std::string name("spinner");
	node->getAttributeString("name", name);

	std::string label;
	node->getAttributeString("label", label);

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

	LLFontGL* font = LLView::selectFont(node);

	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);

	U32 precision = 3;
	node->getAttributeU32("decimal_digits", precision);
	
	S32 label_width = llmin(40, rect.getWidth() - 40);
	node->getAttributeS32("label_width", label_width);

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

	LLUICtrlCallback callback = NULL;

	if(label.empty())
	{
		label.assign( node->getValue() );
	}

	LLSpinCtrl* spinner = new LLSpinCtrl(name,
							rect,
							label,
							font,
							callback,
							NULL,
							initial_value, 
							min_value, 
							max_value, 
							increment,
							LLStringUtil::null,
							label_width);

	spinner->setPrecision(precision);

	spinner->initFromXML(node, parent);
	spinner->setAllowEdit(allow_text_entry);

	return spinner;
}

