/** 
 * @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"
#include "lluictrlfactory.h"

const U32 MAX_STRING_LENGTH = 32;

static LLDefaultChildRegistry::Register<LLSpinCtrl> r2("spinner");

LLSpinCtrl::Params::Params()
:	label_width("label_width"),
	decimal_digits("decimal_digits"),
	allow_text_entry("allow_text_entry", true),
	text_enabled_color("text_enabled_color"),
	text_disabled_color("text_disabled_color"),
	up_button("up_button"),
	down_button("down_button")
{}

LLSpinCtrl::LLSpinCtrl(const LLSpinCtrl::Params& p)
:	LLF32UICtrl(p),
	mLabelBox(NULL),
	mbHasBeenSet( FALSE ),
	mPrecision(p.decimal_digits),
	mTextEnabledColor(p.text_enabled_color()),
	mTextDisabledColor(p.text_disabled_color())
{
	static LLUICachedControl<S32> spinctrl_spacing ("UISpinctrlSpacing", 0);
	static LLUICachedControl<S32> spinctrl_btn_width ("UISpinctrlBtnWidth", 0);
	static LLUICachedControl<S32> spinctrl_btn_height ("UISpinctrlBtnHeight", 0);
	S32 top = getRect().getHeight();
	S32 bottom = top - 2 * spinctrl_btn_height;
	S32 centered_top = top;
	S32 centered_bottom = bottom;
	S32 btn_left = 0;
	// reserve space for spinner
	S32 label_width = llclamp(p.label_width(), 0, llmax(0, getRect().getWidth() - 40));

	// Label
	if( !p.label().empty() )
	{
		LLRect label_rect( 0, centered_top, label_width, centered_bottom );
		LLTextBox::Params params;
		params.name("SpinCtrl Label");
		params.rect(label_rect);
		params.initial_value(p.label());
		if (p.font.isProvided())
		{
			params.font(p.font);
		}
		mLabelBox = LLUICtrlFactory::create<LLTextBox> (params);
		addChild(mLabelBox);

		btn_left += label_rect.mRight + spinctrl_spacing;
	}

	S32 btn_right = btn_left + spinctrl_btn_width;
	
	// Spin buttons
	LLButton::Params up_button_params(p.up_button);
	up_button_params.rect
					.left(btn_left)
					.top(top)
					.right(btn_right)
					.height(spinctrl_btn_height);
	up_button_params.click_callback.function(boost::bind(&LLSpinCtrl::onUpBtn, this, _2));
	up_button_params.mouse_held_callback.function(boost::bind(&LLSpinCtrl::onUpBtn, this, _2));

	mUpBtn = LLUICtrlFactory::create<LLButton>(up_button_params);
	addChild(mUpBtn);

	LLRect down_rect( btn_left, top - spinctrl_btn_height, btn_right, bottom );

	LLButton::Params down_button_params(p.down_button);
	down_button_params.rect
					.left(btn_left)
					.right(btn_right)
					.bottom(bottom)
					.height(spinctrl_btn_height);
	down_button_params.click_callback.function(boost::bind(&LLSpinCtrl::onDownBtn, this, _2));
	down_button_params.mouse_held_callback.function(boost::bind(&LLSpinCtrl::onDownBtn, this, _2));
	mDownBtn = LLUICtrlFactory::create<LLButton>(down_button_params);
	addChild(mDownBtn);

	LLRect editor_rect( btn_right + 1, centered_top, getRect().getWidth(), centered_bottom );
	LLLineEditor::Params params;
	params.name("SpinCtrl Editor");
	params.rect(editor_rect);
	if (p.font.isProvided())
	{
		params.font(p.font);
	}
	params.max_length_bytes(MAX_STRING_LENGTH);
	params.commit_callback.function((boost::bind(&LLSpinCtrl::onEditorCommit, this, _2)));
	params.prevalidate_callback(&LLLineEditor::prevalidateFloat);
	params.follows.flags(FOLLOWS_LEFT | FOLLOWS_BOTTOM);
	mEditor = LLUICtrlFactory::create<LLLineEditor> (params);
	mEditor->setFocusReceivedCallback( boost::bind(&LLSpinCtrl::onEditorGainFocus, _1, 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);
	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;
}


void LLSpinCtrl::onUpBtn( const LLSD& data )
{
	if( getEnabled() )
	{
		std::string text = mEditor->getText();
		if( LLLineEditor::postvalidateFloat( text ) )
		{
			
			LLLocale locale(LLLocale::USER_LOCALE);
			F32 cur_val = (F32) atof(text.c_str());
		
			// use getValue()/setValue() to force reload from/to control
			F32 val = cur_val + mIncrement;
			val = clamp_precision(val, mPrecision);
			val = llmin( val, mMaxValue );
			if (val < mMinValue) val = mMinValue;
			if (val > mMaxValue) val = mMaxValue;
		
			F32 saved_val = (F32)getValue().asReal();
			setValue(val);
			if( !mValidateSignal( this, val ) )
			{
				setValue( saved_val );
				reportInvalidData();
				updateEditor();
				return;
			}

		updateEditor();
		onCommit();
		}
	}
}

void LLSpinCtrl::onDownBtn( const LLSD& data )
{
	if( getEnabled() )
	{
		std::string text = mEditor->getText();
		if( LLLineEditor::postvalidateFloat( text ) )
		{

			LLLocale locale(LLLocale::USER_LOCALE);
			F32 cur_val = (F32) atof(text.c_str());
		
			F32 val = cur_val - mIncrement;
			val = clamp_precision(val, mPrecision);
			val = llmax( val, mMinValue );

			if (val < mMinValue) val = mMinValue;
			if (val > mMaxValue) val = mMaxValue;
			
			F32 saved_val = (F32)getValue().asReal();
			setValue(val);
			if( !mValidateSignal( this, val ) )
			{
				setValue( saved_val );
				reportInvalidData();
				updateEditor();
				return;
			}
		
			updateEditor();
			onCommit();
		}
	}
}

// static
void LLSpinCtrl::onEditorGainFocus( LLFocusableElement* caller, void *userdata )
{
	LLSpinCtrl* self = (LLSpinCtrl*) userdata;
	llassert( caller == self->mEditor );

	self->onFocusReceived();
}

void LLSpinCtrl::setValue(const LLSD& value )
{
	F32 v = (F32)value.asReal();
	if (getValueF32() != v || !mbHasBeenSet)
	{
		mbHasBeenSet = TRUE;
        LLF32UICtrl::setValue(value);
		
		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 (getValueF32() != v || !mbHasBeenSet)
	{
		mbHasBeenSet = TRUE;
        LLF32UICtrl::setValue(value);
		
		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( const LLSD& data )
{
	BOOL success = FALSE;
	
	std::string text = mEditor->getText();
	if( LLLineEditor::postvalidateFloat( text ) )
	{
		LLLocale locale(LLLocale::USER_LOCALE);
		F32 val = (F32) atof(text.c_str());

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

		F32 saved_val = getValueF32();
		setValue(val);
		if( mValidateSignal( this, val ) )
		{
			success = TRUE;
			onCommit();
		}
		else
		{
			setValue(saved_val);
		}
	}
	updateEditor();

	if( !success )
	{
		reportInvalidData();		
	}
}


void LLSpinCtrl::forceEditorCommit()
{
	onEditorCommit( LLSD() );
}


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

void LLSpinCtrl::setEnabled(BOOL b)
{
	LLView::setEnabled( b );
	mEditor->setEnabled( b );
	if( mLabelBox )
	{
		mLabelBox->setColor( b ? mTextEnabledColor.get() : mTextDisabledColor.get() );
	}
}


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(getValueF32());
	LLF32UICtrl::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;
	}
}

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

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

	return TRUE;
}

BOOL LLSpinCtrl::handleKeyHere(KEY key, MASK mask)
{
	if (mEditor->hasFocus())
	{
		if(key == KEY_ESCAPE)
		{
			// 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)
		{
			onUpBtn(getValue());
			return TRUE;
		}
		if(key == KEY_DOWN)
		{
			onDownBtn(getValue());
			return TRUE;
		}
	}
	return FALSE;
}