// ------------------------------------------------------------------------
/*
	author: Jaap Suter

	Copyright (C) Electronic Arts Canada Inc. 2002.  All rights reserved.	
*/
// ------------------------------------------------------------------------

/*! \file */

#ifndef REVEAL_RANGE_CONTROL_H
#define REVEAL_RANGE_CONTROL_H

#include "reveal/impl/widget.h"
#include "reveal/impl/gui_metrics.h"
#include "reveal/impl/util.h"

#include "reveal/input_state.h"
#include "reveal/renderer.h"

#include "reveal/callback/callback_0.h"
#include "reveal/callback/callback_1.h"
#include "reveal/type_traits/type_traits.h"

#if defined (_MSC_VER)
#pragma warning( disable : 4127 )   // conditional expression is constant
#endif

#include <assert.h>

namespace Reveal {
	namespace Impl {

// -------------------------------------------------------------------------------
/*!
	\brief Range controller widget;

	\param T type of the range you want to control (char, int, float, whatever, non-built-in, etc.)
*/
// -------------------------------------------------------------------------------
template < class T >
class RangeControl : public Widget
{
    typedef Callback::Callback1< void, T > CallbackToGame;
	typedef Callback::Callback0< T >	   CallbackFromGame;

	enum
	{
		HOLD_SCALE_DIVIDER = 10000,
		HOLD_DURATION_UNTIL_REPEAT = 20,
	};

public:

	/*!
		\brief Constructor
		\param name		Name of the control;
		\param min		Instance of T representing the minimum value (inclusive);
		\param max		Instance of T representing the maximum value (inclusive);
		\param callback Callback that needs to be called whenever the current value changes;
	*/
    RangeControl( const char* name, Tree& tree, T min, T max, const CallbackToGame& callbackToGame, const CallbackFromGame& callbackFromGame, const float stepSize )
        : Widget( name, tree ),
		  mCallbackToGame( callbackToGame ),
		  mCallbackFromGame( callbackFromGame ),
		  mMin( min ), mMax( max ),
		  mValue( min + max / 2 ),
		  mState( IDLE ),
		  mHold( false ),
		  mHoldScale( static_cast< float >( max - min ) / static_cast< float >( HOLD_SCALE_DIVIDER ) ),
		  mHoldDuration( 0 ),
			mStepSize(stepSize)
	{
		assert( max >= min );

		SetHelp( "Mouse drag: Change value\n"
				"Aux analog: Change value\n"
				"Arrow keys: Step value\n"
				"+ Aux1: Step value x 2\n"
				"+ Aux2: Step value x 8\n" );
	}

    //! Returns the width of the widget;
	virtual int  Width() const { return Impl::GuiMetrics::GetWidgetWidth(); }

	//! Returns the height of the widget;
	virtual int  Height() const { return NoSlider() ? Impl::GuiMetrics::GetRowHeight() : NUM_ROWS * Impl::GuiMetrics::GetRowHeight(); }

private:

	// Copy ctor and assignment op not implemented;
	RangeControl( const RangeControl& );
	RangeControl& operator = ( const RangeControl& );

	// Class constants;
	enum
	{
		NUM_ROWS			=  2, //!< Title-bar and actual slider;
		MAX_NO_SLIDER_VALUE = 12, //!< Just enough for a soccer team;
		TWO_CHARACTER_WIDTH = 30  //!< Number of pixels needed to display two digits;
	};

	//! Renders the control;
    void DoRender( const Renderer& renderer, const GuiColors& guiColors ) const;

	//! Handles the input;
	virtual Widget* DoHandle( const InputState& inputState );

	//! Outputs the value to a string;
	size_t DoToString( const char* path, char* buffer, size_t size ) const;

	//! Returns the horizontal GUI space position of the top left corner of the left slider arrow;
	int LeftArrowX()     const { return NoSlider() ? RightArrowX() - TWO_CHARACTER_WIDTH - GuiMetrics::GetArrowBoxSize() : X() + GuiMetrics::BORDER_WIDTH; }

	//! Returns the horizontal GUI space position of the top left corner of the right slider arrow;
	int RightArrowX()    const { return X() + (NoSlider() ? Width() : (Width() - GuiMetrics::GetCharacterWidth() * 4)) - GuiMetrics::GetArrowBoxSize() - GuiMetrics::BORDER_WIDTH; }

	//! Returns the vertical GUI space position of the two slider arrows;
	int ArrowsY()	     const { return Y() + GuiMetrics::BORDER_HEIGHT + (NoSlider() ? 0 : GuiMetrics::GetRowHeight()); }

	//! Returns the horizontal GUI space position of the top left corner of the slider bar;
	int SliderBarX()	 const { return LeftArrowX() + GuiMetrics::GetArrowBoxSize(); }

	//! Returns the width of the slider bar;
	int SliderBarWidth() const { return RightArrowX() - SliderBarX(); }

	//! Returns true if the range control should not have a slider bar, but be just two arrows with a value in between;
	bool NoSlider() const { return TypeTraits::IsIntegral< T >::value && ((mMax - mMin) < MAX_NO_SLIDER_VALUE); }
	
	// Implementation of base virtual;
	virtual void DoLoseFocus() { mState = IDLE; }
	
	//! Keeps the current value within the valid range;
	void KeepValueWithinRange() { if ( mValue < mMin ) mValue = mMin; if ( mValue > mMax ) mValue = mMax; }

	CallbackToGame   mCallbackToGame;	//!< Callback, called whenever the current value changes;
	CallbackFromGame mCallbackFromGame;	//!< Callback, called whenever the latest value is needed, if it is valid, that is.
	T mMin;								//!< Minimum value of the range, inclusive;
	T mMax;								//!< Maximum value of the range, inclusive;
	T mValue;							//!< Current value in the range;
    enum { IDLE, DRAGGING  } mState;	//!< Current state of the range control;

	bool  mHold;
	float mHoldScale;
	int   mHoldDuration;
	float mStepSize;

	/*!
		\brief Changes the value by a certain percentage;

		\note If the absolute change in the value is smaller than one, and the type T is
			  integral, we round it to 1 or -1 depending on the sign;

	    \note Defined inline, because GCC complains otherwise;
	*/
	void ChangeValueByPercentage( float signedPercentage )
	{	
		float delta = signedPercentage * static_cast< float >( mMax - mMin ) / 100.0f;
		if ( TypeTraits::IsIntegral< T >::value )
			if ( (delta > -1) && (delta < 1) )
				delta = signedPercentage < 0.0f ? -1.0f : 1.0f;
		
		ChangeValueByDelta( delta );
	}

	//! Changes the value by a delta;
	void ChangeValueByDelta( float delta )
	{	
		if ( static_cast< float >( mValue ) < (static_cast< float >( mMin ) - delta) )
			mValue = mMin;
		else
		{
			mValue = static_cast< T >( static_cast< float >( mValue ) + delta );
			KeepValueWithinRange();
		}
		mCallbackToGame( mValue );
	}

	//! Updates the value from game.
	virtual void Update()
	{
		if ( mCallbackFromGame.Valid() ) mValue = mCallbackFromGame();
	}
};

// -------------------------------------------------------------------------------
// Implementation;
// -------------------------------------------------------------------------------

// -------------------------------------------------------------------------------
template < class T >
void RangeControl< T >::DoRender( const Renderer& renderer, const GuiColors& guiColors ) const
{
	Reveal::uint32_t bgColor= guiColors.mWidgetBackgroundColor;
	if ( GetTree().GetWidgetWithFocus() && GetTree().GetWidgetWithFocus()->Parent() == Parent() ) bgColor= guiColors.mWidgetSameLevelBackgroundColor;
	if ( HasFocus() ) bgColor= guiColors.mWidgetHighlightedBackgroundColor;

    renderer.Bar( X(), Y(), Width(), Height(), bgColor );
	renderer.String( X() + GuiMetrics::TEXT_BORDER_WIDTH, Y() + GuiMetrics::TEXT_BORDER_HEIGHT, Name(), guiColors.mTextColor );

	renderer.Bar(	   LeftArrowX(), ArrowsY(), GuiMetrics::GetArrowBoxSize(), GuiMetrics::GetItemSize(), guiColors.mInteractiveBackgroundColor );
	renderer.Triangle( LeftArrowX() + GuiMetrics::GetArrowBoxSize() - GuiMetrics::BORDER_WIDTH, ArrowsY() + GuiMetrics::BORDER_HEIGHT,
						LeftArrowX() + GuiMetrics::GetArrowBoxSize() - GuiMetrics::BORDER_WIDTH, ArrowsY() + GuiMetrics::GetItemSize() - GuiMetrics::BORDER_HEIGHT,
						LeftArrowX() + GuiMetrics::BORDER_WIDTH,
						ArrowsY() + GuiMetrics::BORDER_HEIGHT + (GuiMetrics::GetItemSize() - 2 * GuiMetrics::BORDER_HEIGHT) / 2,
						guiColors.mInteractiveForegroundColor );
	renderer.Bar(	   RightArrowX(), ArrowsY(), GuiMetrics::GetArrowBoxSize(), GuiMetrics::GetItemSize(), guiColors.mInteractiveBackgroundColor );
	renderer.Triangle( RightArrowX() + GuiMetrics::BORDER_WIDTH, ArrowsY() + GuiMetrics::BORDER_HEIGHT,
						RightArrowX() + GuiMetrics::BORDER_WIDTH, ArrowsY() + GuiMetrics::GetItemSize() - GuiMetrics::BORDER_HEIGHT,
						RightArrowX() + GuiMetrics::GetArrowBoxSize() - GuiMetrics::BORDER_WIDTH,
						ArrowsY() + GuiMetrics::BORDER_HEIGHT + (GuiMetrics::GetItemSize() - 2 * GuiMetrics::BORDER_HEIGHT) / 2,
						guiColors.mInteractiveForegroundColor );		

	if ( ! NoSlider() )
	{
		int sliderX = LeftArrowX() + GuiMetrics::GetArrowBoxSize() + static_cast< int >( static_cast< T >( (SliderBarWidth() - GuiMetrics::GetSliderSize()) * (mValue - mMin) / (mMax - mMin) ) );
		renderer.Bar( SliderBarX(), ArrowsY() + 8, SliderBarWidth(), GuiMetrics::GetItemSize() - 16, mState == DRAGGING ? guiColors.mInteractiveForegroundColor : guiColors.mInteractiveBackgroundColor );
		renderer.Bar( sliderX, ArrowsY(), GuiMetrics::GetSliderSize(), GuiMetrics::GetItemSize(), guiColors.mSliderColor );
	}

	const int maxStringLength = 32;
	char numberRepr[ maxStringLength ];
	if ( TypeTraits::IsIntegral< T >::value )
		Util::ToString( static_cast< int >( mValue ), numberRepr, maxStringLength );
	else
		Util::ToString( static_cast< float >( mValue ), numberRepr, maxStringLength );
	if ( NoSlider() )
		renderer.String( LeftArrowX() + GuiMetrics::BORDER_WIDTH + GuiMetrics::GetArrowBoxSize(), ArrowsY(), numberRepr, guiColors.mTextColor );
	else
		renderer.String( RightArrowX() + GuiMetrics::BORDER_WIDTH + GuiMetrics::GetArrowBoxSize(), ArrowsY(), numberRepr, guiColors.mTextColor );

	DrawBorder( renderer, guiColors );
}

// -------------------------------------------------------------------------------
template < class T >
Widget* RangeControl< T >::DoHandle( const InputState& inputState )
{
	Widget* widgetThatHandledTheInput = NULL;
	
	if ( mCallbackFromGame.Valid() ) mValue = mCallbackFromGame();

	if ( HasFocus() )
	{
		widgetThatHandledTheInput = this;

        if ( mState == DRAGGING )
		{
			if ( inputState.Released( InputState::LEFT_MOUSE ) )
			{
				mState = IDLE;
				MakeParentMostRecentlyUsed();
			}
			
			if ( Util::PointInRect( inputState.MouseX(), inputState.MouseY(), X(), Y(), Width(), Height() ) )
			{
				if ( inputState.MouseMove() || inputState.Released( InputState::LEFT_MOUSE ) )
				{
					mValue = mMin + static_cast< T >( (mMax - mMin) * (inputState.MouseX() - SliderBarX()) / (SliderBarWidth() - GuiMetrics::GetArrowBoxSize()) );
					KeepValueWithinRange();
					mCallbackToGame( mValue );
				}
			}
		}
		else
		{
			if ( inputState.Released( InputState::UP ) )
			{					
				if ( Parent() ) widgetThatHandledTheInput = Parent()->GiveFocusToPrevWidget( this );
			}
			else
			if ( inputState.Released( InputState::DOWN ) )
			{
				if ( Parent() ) widgetThatHandledTheInput = Parent()->GiveFocusToNextWidget( this );
			}
			else
			if ( inputState.Down( InputState::LEFT ) )
			{
				MakeParentMostRecentlyUsed();
				if ( inputState.Down( InputState::AUX_1 ) ) ChangeValueByPercentage( -(mStepSize * 2) ); else
				if ( inputState.Pressed( InputState::LEFT ) )
				{
					if ( TypeTraits::IsFloatingPoint< T >::value )
						ChangeValueByPercentage( -mStepSize );
					else
						ChangeValueByDelta( -1 );
				}
			}
			else
			if ( inputState.Down( InputState::RIGHT ) )
			{
				MakeParentMostRecentlyUsed();
				if ( inputState.Down( InputState::AUX_1 ) ) ChangeValueByPercentage( (mStepSize * 2) ); else
				if ( inputState.Pressed( InputState::RIGHT ) )
				{
					if ( TypeTraits::IsFloatingPoint< T >::value )
						ChangeValueByPercentage( mStepSize );
					else
						ChangeValueByDelta( 1 );
				}
			}
			else
			if ( inputState.AuxRelX() )
			{
				MakeParentMostRecentlyUsed();

				//assumes a return of -128/128 to give final range of -8/8
				int val = (inputState.AuxRelX() / 16);
				if ( TypeTraits::IsFloatingPoint< T >::value )
					ChangeValueByPercentage( static_cast<float> (val) );
				else
					ChangeValueByDelta( static_cast<float> (val) );
			}
			else				
			if ( inputState.Released( InputState::DOWN ) )
			{
				mState = DRAGGING;
			}
			else
			if ( inputState.Released( InputState::BACK ) )
			{					
				widgetThatHandledTheInput = GiveFocusToParent();
			}
			else
			if ( inputState.Down( InputState::CONFIRM | InputState::LEFT_MOUSE ) )
			{
				if ( Util::PointInRect( inputState.MouseX(), inputState.MouseY(), LeftArrowX(), ArrowsY(),
										Impl::GuiMetrics::GetArrowBoxSize(), Impl::GuiMetrics::GetItemSize() ) )
				{
					if ( inputState.Down( InputState::AUX_1 ) )
						ChangeValueByPercentage( -(mStepSize * 2) );
					else
					if ( TypeTraits::IsFloatingPoint< T >::value )
						ChangeValueByPercentage( -(mStepSize * mHoldScale) );
					else
					if ( mHoldDuration == 0 || mHoldDuration > HOLD_DURATION_UNTIL_REPEAT )
						ChangeValueByDelta( -1 );
					
					mHold = true;
				}
				else
				if ( Util::PointInRect( inputState.MouseX(), inputState.MouseY(), RightArrowX(), ArrowsY(),
										Impl::GuiMetrics::GetArrowBoxSize(), Impl::GuiMetrics::GetItemSize() ) )
				{
					if ( inputState.Down( InputState::AUX_1 ) )
						ChangeValueByPercentage( (mStepSize * 2) );
					else
					if ( TypeTraits::IsFloatingPoint< T >::value )
						ChangeValueByPercentage( (mStepSize * mHoldScale) );
					else
					if ( mHoldDuration == 0 || mHoldDuration > HOLD_DURATION_UNTIL_REPEAT )
						ChangeValueByDelta( 1 );
					
					mHold = true;
				}
				else
				if ( ! NoSlider() )
				{
					if ( Util::PointInRect( inputState.MouseX(), inputState.MouseY(), SliderBarX(), ArrowsY(), SliderBarWidth(), GuiMetrics::GetItemSize() ) )
					{
						mState = DRAGGING;
					}
				}
				MakeParentMostRecentlyUsed();
			}
			else
			if ( inputState.Released( InputState::CONFIRM | InputState::LEFT_MOUSE ) )
			{
				MakeParentMostRecentlyUsed();
				mHold = false;
				mHoldScale = static_cast< float >( mMax - mMin ) / static_cast< float >( HOLD_SCALE_DIVIDER );
				mHoldDuration = 0;
			}
		}
	}

	if ( mHold )
	{
		float acceleration = 1.2f;
		mHoldScale *= acceleration;
		if ( mHoldScale >= 1.0f ) mHoldScale = 1.0f;
		++mHoldDuration;
	}

	return widgetThatHandledTheInput;
}

// -------------------------------------------------------------------------------
template < class T >
size_t RangeControl< T >::DoToString( const char* path, char* buffer, size_t size ) const
{
	const size_t pathAndNameLength = PathAndNameToString( path, buffer, size );
	assert( size >= pathAndNameLength );
	size -= pathAndNameLength;
	buffer += pathAndNameLength;	

	if ( size >= 1 )
	{
		*buffer++ = '\"';
		--size;
	}
	else return pathAndNameLength;

	const size_t valueLength = TypeTraits::IsIntegral< T >::value ?
		Util::ToString( static_cast< int >( mValue ), buffer, size )
			:
		Util::ToString( static_cast< float >( mValue ), buffer, size );

	
	assert( size >= valueLength );
	size -= valueLength;
	buffer += valueLength;
	
	if ( size >= 1 )
	{
		*buffer++ = '\"';
		--size;
	} else return valueLength + pathAndNameLength + 1;

	*buffer = '\n';

	return pathAndNameLength + valueLength + 3;
}

	} // Namespace Impl;
} // Namespace Reveal;

#endif
