/******************************************************************************
 * $Id: gfx_gui.cc,v 1.82 2006/08/10 03:18:58 devel Exp $
 * @file
 * @author	Jim E. Brooks  http://www.jimbrooks.org
 * @brief	Simple lightweight GUI inside of a viewport.
 *          Provides GUI objects such as text fonts, buttons, etc.
 *          GUI objects are drawn alpha-blended over prior rendering.
 * @remarks The source code here is (should be) system-neutral.
 *//*
 * LEGAL:	COPYRIGHT (C) 2004 JIM E. BROOKS
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE (GPL).
 ******************************************************************************/

#define GFX_GUI_CC 1
#include "gfx.hh"
#include "gfx_stream.hh"
#include "gfx_font.hh"
#include "gfx_gui.hh"
namespace gfx {

static const RGBA sRgbaDefault( 1.0, 1.0, 1.0, 1.0 );

////////////////////////////////////////////////////////////////////////////////
///////////////////////////////  GuiBase class  ////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

// --------------------------------------------------------------------------
// Macros for GuiBase class.  For every object in every screen.
// --------------------------------------------------------------------------
#define BEGIN_FOR_EACH_SCREEN_EACH_OBJECT                               	\
CHECK_TYPESIG( &mScreenDlist, TYPESIG_LIB_DLIST );                      	\
	for ( const Dlink<GuiScreen>* dlinkScreen = mScreenDlist.ForStart();    \
	      mScreenDlist.ForTest(dlinkScreen);                            	\
          mScreenDlist.ForNext(dlinkScreen) )                       		\
	{                                                           			\
CHECK_TYPESIG( dlinkScreen, TYPESIG_LIB_DLINK );            				\
		GuiScreen* screen = dlinkScreen->mObj;          					\
CHECK_TYPESIG( &screen->mObjectDlist, TYPESIG_LIB_DLIST );          		\
		for ( const Dlink<GuiObject>* dlinkObj = screen->mObjectDlist.ForStart(); \
		      screen->mObjectDlist.ForTest(dlinkObj);						\
			  screen->mObjectDlist.ForNext(dlinkObj) )						\
		{                                                               	\
CHECK_TYPESIG( dlinkObj, TYPESIG_LIB_DLINK );                             	\
			GuiObject* obj = dlinkObj->mObj;
// --------------------------------------------------------------------------
// FYI:		GuiObject*& obj = dlinkObj->mObj;  // oops
//			GuiObject* obj = dlinkObj->mObj;   // ok
// The reference form has a bad side-effect:
// dlist->Unlink(obj) will be called which will delete dlinkObj!
// --------------------------------------------------------------------------
#define END_FOR_EACH_SCREEN_EACH_OBJECT                                 	\
		}                                                               	\
	}
// --------------------------------------------------------------------------

/*****************************************************************************
 * Constructor.
 *****************************************************************************/
GuiBase::GuiBase( void )
{
	DEBUG_CODE( if (!GFX_IF_READY()) *gStreamDebug << "GuiBase: gfxsys not ready \n"; )
	mViewport.Clear();			// to be assigned by derived Gui class
	mViewportInitial.Clear();	// to be assigned by derived Gui class
	mZoom					= 1.0;
	mZoomViewport			= 1.0;
	mViewportDelta[0]		= 0;
	mViewportDelta[1]		= 0;
	mMousePos.x				= -999;
	mMousePos.y				= -999;
	mMouseButtons			= 0;
	mLfontText				= NULL;		// to be assigned by derived Gui class
	mLfontTextThin			= NULL;		// to be assigned by derived Gui class
	mLfontButton			= NULL;		// to be assigned by derived Gui class
}
GuiBase::~GuiBase()
{
    CHECK_TYPESIG( &mScreenDlist, TYPESIG_LIB_DLIST );
}

/*****************************************************************************
 * Render all screens.
 *****************************************************************************/
void
GuiBase::Render( void )
{
CHECK_TYPESIG( &mScreenDlist, TYPESIG_LIB_DLIST );
	for ( const Dlink<GuiScreen>* dlink = mScreenDlist.ForStart();
          mScreenDlist.ForTest(dlink);
          mScreenDlist.ForNext(dlink) )
	{
CHECK_TYPESIG( dlink, TYPESIG_LIB_DLINK );
		dlink->mObj->Render(); // GuiScreen::Render()
	}
}

/*****************************************************************************
 * Return a new "screen".  A screen is a set of GUI objects.
 *****************************************************************************/
GuiScreen*
GuiBase::NewScreen( void )
{
CHECK_TYPESIG( &mScreenDlist, TYPESIG_LIB_DLIST );
	GuiScreen* screen = new GuiScreen( *this );
	screen->mLinkScreen = mScreenDlist.Append( screen );
	return screen;
}
void
GuiBase::DeleteScreen( GuiScreen* screen )
{
CHECK_TYPESIG( &mScreenDlist, TYPESIG_LIB_DLIST );
	mScreenDlist.Unlink( screen->mLinkScreen );
	screen->mLinkScreen = NULL;
	delete screen;
}

/*****************************************************************************
 * Viewport and reshape.
 * The user must call ReshapeView() whenever the viewport is reshaped
 * so that the GUI object can adjust accordingly.
 *****************************************************************************/
void
GuiBase::ReshapeViewport( const Geom<int>& viewport, const Geom<int>& window )
{
	mViewport.Copy( viewport );
	mWindow.Copy( window );

	if ( ABS(mViewportInitial.w) > 1.0 ) // avoid divide-by-zero
		mZoomViewport = mViewport.w / mViewportInitial.w;
	else
		mZoomViewport = 1.0;

	// Deltas to compensate for viewport being offset within main window.
	mViewportDelta[0] = (mWindow.w - mViewport.w) + (mWindow.x - mViewport.x);
	mViewportDelta[1] = (mWindow.h - mViewport.h) + (mWindow.y - mViewport.y);
}

/*****************************************************************************
 * Your application is supposed to pass mouse events to this.
 * @param	x,y
 *			Mouse position.
 *****************************************************************************/
void
GuiBase::AppMouseMotionHandler( float x, float y )
{
	SetMousePos( x, y );
}

/*****************************************************************************
 * Your application is supposed to pass mouse events to this.
 * This is for a single button event.
 * @param	button
 *			Which button (singular): eMouseEvent_BUTTON_[123]
 * @param	down
 *          True if button clicked, false if released.
 *****************************************************************************/
void
GuiBase::AppMouseButtonHandler( eMouseEvent button, bool down )
{
BUSY_BEGIN;
	uint allButtons = GetMouseButtons();
	allButtons &= ~button;					// clear bit
	if ( down ) allButtons |= button;		// set bit if button down
	SetMouseButtons( allButtons );
BUSY_END;
}

/*****************************************************************************
 * @return True if mouse is inside viewport.
 *****************************************************************************/
bool
GuiBase::GetMousePos( float& x /*OUT*/, float& y /*OUT*/ )
{
	if (    mMousePos.x >= mViewport.x
         && mMousePos.x <= mViewport.x + mViewport.w
         && mMousePos.y >= mViewport.y
         && mMousePos.y <= mViewport.y + mViewport.h )
	{
		x = mMousePos.x;  // output by ref
		y = mMousePos.y;
		return true;
	}
	return false;
}

/*****************************************************************************
 * Mouse was moved.
 *****************************************************************************/
void
GuiBase::SetMousePos( const float x, const float y )
{
	mMousePos.x = x;
	mMousePos.y = y;
	MouseHandler( true );
}

/*****************************************************************************
 * Get/set mouse button bit fields.
 * Use a combination of eMouseEvent_BUTTON_*.
 *****************************************************************************/
uint
GuiBase::GetMouseButtons( void )
{
	return mMouseButtons;
}
void
GuiBase::SetMouseButtons( uint buttons )
{
	mMouseButtons = buttons;
	MouseHandler( false );
}

/*****************************************************************************
 * Mouse handler.
 *****************************************************************************/
void
GuiBase::MouseHandler( bool motionEvent )
{
	if ( UX( ! GFX_IF_READY() ) ) return;

BUSY_BEGIN;

	int redisplay = 0;

	// For each object in every screen, determine if the mouse has moved over its area.
	// Only state transitions are of interest, eg, the moment when the mouse moves
	// from outside to inside or v.v.

//--------------------------------
BEGIN_FOR_EACH_SCREEN_EACH_OBJECT;
//--------------------------------

	int prev = obj->mMouseEvent;

	// Translate mouse position into a 2D viewport coord.
	Pos2d<float> pos;
	MousePos2ViewportPos( mMousePos, pos);

	if ( motionEvent )
	{
		// Enter transition?
		// Yes if event wasn't enter and mouse position is inside.
		if (    ! (prev & eMouseEvent_MOTION_ENTER)
                && obj->mRect.IfInside(pos.x,pos.y) )
		{
			obj->mMouseEvent |=  eMouseEvent_MOTION_ENTER;
			obj->mMouseEvent &= ~eMouseEvent_MOTION_EXIT;
			if ( obj->IfVisi() ) ++redisplay;
		}
		// Exit transition?
		// Yes if event was enter and mouse position isn't inside.
		else if (     (prev & eMouseEvent_MOTION_ENTER)
                      && ! obj->mRect.IfInside(pos.x,pos.y) )
		{
			obj->mMouseEvent |=  eMouseEvent_MOTION_EXIT;
			obj->mMouseEvent &= ~eMouseEvent_MOTION_ENTER;
			if ( obj->IfVisi() ) ++redisplay;
		}
	}
	else // button event
	{
		if ( obj->IfVisi() ) ++redisplay;

		// Merge button state into object's event member.
		obj->mMouseEvent &= ~eMouseEvent_BUTTON_MASK;
		obj->mMouseEvent |= mMouseButtons;

		// Is mouse inside this object's area?
		if ( obj->mRect.IfInside(pos.x,pos.y) )
		{
			// Did button #1 transition from released to pressed?
			if (      (mMouseButtons & eMouseEvent_BUTTON_1)
                 && ! (prev & eMouseEvent_BUTTON_1) )
			{
				if (    obj->mMouseButtonCallback != NULL
                     && obj->IfSens() )
 					 obj->mMouseButtonCallback( obj, mMouseButtons );
			}
		}
	}

//------------------------------
END_FOR_EACH_SCREEN_EACH_OBJECT;
//------------------------------

	// Redisplay is needed if a mouse event occured in
	// an object's area and the object is visible.
	if ( redisplay ) GFX_POST_REDISPLAY();

BUSY_END;
}

/*****************************************************************************
 * Common helper to get appropriate Font object.
 *****************************************************************************/
FontPtrc
GuiBase::GetFont( eScreenFontType type )
{
	switch ( type )
	{
		case eScreenFontType_NORMAL: default:
		return mLfontText;
		break;

		case eScreenFontType_THIN:
		return mLfontTextThin;
		break;
	}
	return mLfontText; // default, shutup compiler
}

/*****************************************************************************
 * Timer callback invoked every second.
 *****************************************************************************/
void
GuiBase::AppTimerOneSecond( void )
{
	if ( UX( ! GFX_IF_READY() ) ) return;

BUSY_BEGIN;

	int redisplay = 0;

//--------------------------------
BEGIN_FOR_EACH_SCREEN_EACH_OBJECT;
//--------------------------------

	switch ( obj->mType )
	{
		case eGuiObject_PRINT:
		{
			// A message created by GuiScreen::PrintTimer() has expired.
			// Unlink the GuiPrint object out of the screen's object list.
			GuiPrint* printobj = static_cast<GuiPrint*>(obj);
			if ( --printobj->mTimer < 0 )
			{
				screen->UnlinkObject( obj );
				delete obj;
				--screen->mPrintTimerCnt;
				++redisplay;
			}
		}
		break;

		default:
		break;
	}

//------------------------------
END_FOR_EACH_SCREEN_EACH_OBJECT;
//------------------------------

	if ( redisplay ) GFX_POST_REDISPLAY();

BUSY_END;
}

////////////////////////////////////////////////////////////////////////////////
/////////////////////////////  GuiScreen class  ////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

// ------------------------------------------------------------------
// For each GuiObject in one list of one GuiScreen.
// ------------------------------------------------------------------
#define BEGIN_FOR_EACH_OBJECT										\
CHECK_TYPESIG( &mObjectDlist, TYPESIG_LIB_DLIST );					\
	for ( const Dlink<GuiObject>* dlink = mObjectDlist.ForStart();	\
          mObjectDlist.ForTest(dlink);								\
          mObjectDlist.ForNext(dlink) )								\
	{																\
CHECK_TYPESIG( dlink, TYPESIG_LIB_DLINK );							\
		GuiObject* obj = dlink->mObj;
// ------------------------------------------------------------------
#define END_FOR_EACH_OBJECT											\
	}
// ------------------------------------------------------------------

/*****************************************************************************
 * Constructor.
 *****************************************************************************/
GuiScreen::GuiScreen( GuiBase& gui )
:	mGui(gui),
    mVisible(false),
	mSensitive(false),
	mLinkScreen(NULL),  // assigned by Gui::NewScreen()
	mPrintTimerCnt(0),
	mGfont(gui.mLfontText, eScreenFontSize_NORMAL, 1.0, sRgbaDefault)
  //mObjectDlist
  //mGfontStack
{
}
GuiScreen::~GuiScreen()
{
    CHECK_TYPESIG( &mObjectDlist, TYPESIG_LIB_DLIST );
	// mLinkScreen deleted by Gui::DeleteScreen()
}

/*****************************************************************************
 * Render all objects in the screen.
 *****************************************************************************/
void
GuiScreen::Render( void )
{
	if ( mVisible )
	{
		BEGIN_FOR_EACH_OBJECT;
		obj->Render();
		END_FOR_EACH_OBJECT;
	}
}

/*****************************************************************************
 * Return true if any GuiPrint object with an outstanding timer matches
 * the passed string.  Used to avoid printing duplicate messages.
 *****************************************************************************/
bool
GuiScreen::IfPrintTimerDuplicate( const char* str )
{
	if ( PrintTimerPopulation() == 0 ) return false;
	BEGIN_FOR_EACH_OBJECT;
	if ( obj->mType == eGuiObject_PRINT )
	{
		GuiPrint* printobj = static_cast<GuiPrint*>(obj);
		if ( printobj->mTimer > 0  &&  STREQ( str, printobj->mText.c_str() ) )
			return true;
	}
	END_FOR_EACH_OBJECT;
	return false;
}

/*****************************************************************************
 * Manage list of objects in screen.
 *****************************************************************************/
void
GuiScreen::LinkObject( GuiObject* obj )
{
CHECK_TYPESIG( &mObjectDlist, TYPESIG_LIB_DLIST );
	obj->mLinkObject = mObjectDlist.Append( obj );
}
void
GuiScreen::UnlinkObject( GuiObject* obj )
{
CHECK_TYPESIG( &mObjectDlist, TYPESIG_LIB_DLIST );
	mObjectDlist.Unlink( obj->mLinkObject );
	obj->mLinkObject = NULL;
}

/*****************************************************************************
 * Font::Print() wrapper.
 * See gfx_font.hh.
 * Unlike PrintTimer(), the application must repetively call Print() on every frame.
 *****************************************************************************/
int
GuiScreen::Print( const char* text, uint textLen, float x, float y, eFontMode fontMode )
{
	return mGfont.Print( text, textLen, x, y, fontMode );
}
int
GuiScreen::Print( const char* text, uint textLen, float x, float y, eFontMode fontMode, const RGBA rgba )
{
	return mGfont.Print( text, textLen, x, y, fontMode, rgba );
}

/*****************************************************************************
 * Print a message that expires.
 * Unlike regular Print(), the Gui class code is responsible for drawing
 * the message text on every frame.
 *****************************************************************************/
void
GuiScreen::PrintTimer( int seconds, const PrintDesc& desc )
{
	// The base GuiObject will link this to the object list.
	// GuiPrint::Render() will draw it until it expires.
	new GuiPrint( *this, seconds, desc );
	++mPrintTimerCnt;
}

/*****************************************************************************
 * Set color used by Print().
 *****************************************************************************/
void
GuiScreen::SetPrintColor( const RGBA rgba )
{
	mGfont.SetRgba( rgba );
}

/*****************************************************************************
 * Interface to change and restore fonts.
 * PushPrint() does push-then-change operations.
 *****************************************************************************/
void
GuiScreen::PushPrint( eScreenFontType type, eScreenFontSize size )
{
	PushFont( mGfont, type, size, mGfont.GetRgba() );  // keep current RGBA
}
void
GuiScreen::PushPrint( eScreenFontType type, eScreenFontSize size, const RGBA rgba )
{
	PushFont( mGfont, type, size, rgba );  // use specified RGBA
}
void
GuiScreen::PopPrint( void )
{
	PopFont( mGfont );
}

/*****************************************************************************
 * Push/pop fonts in this screen.
 *****************************************************************************/
void
GuiScreen::PushFont( Gfont& gfont, eScreenFontType type, eScreenFontSize size, const RGBA rgba )
{
	DEBUG_CODE( if ( mGfontStack.size() > GUIBASE_PUSH_FONT_SOFT_LIMIT )
                    *gStreamDebug << "GuiBase::PushFont: stack growing too much, are pops missing?\n"; )

	// Push copy of current font.
    mGfontStack.push( gfont );

	// Font type (Font pointer).
	gfont.SetFont( mGui.GetFont( type ) );  // set underlying Font object in Gfont wrapper

	// Font size.
	gfont.SetSize( size );

	// Font zoom.
	const float offs = 0.0;
	switch ( size )
	{
		case eScreenFontSize_SMALLEST:
		gfont.SetZoom( 0.4 + offs );
		break;

		case eScreenFontSize_SMALLER:
		gfont.SetZoom( 0.6 + offs );
		break;

		case eScreenFontSize_SMALL:
		gfont.SetZoom( 0.8 + offs );
		break;

		case eScreenFontSize_NORMAL: default:
		gfont.SetZoom( 1.0 + offs );
		break;

		case eScreenFontSize_LARGE:
		gfont.SetZoom( 1.4 + offs );
		break;

		case eScreenFontSize_LARGER:
		gfont.SetZoom( 1.8 + offs );
		break;

		case eScreenFontSize_LARGEST:
		gfont.SetZoom( 2.2 + offs );
		break;
	}

	// Font color.
	gfont.SetRgba( rgba );
}
void
GuiScreen::PopFont( Gfont& gfont )
{
    ASSERT( ! mGfontStack.empty() );
    gfont = mGfontStack.top(); mGfontStack.pop();  // top,pop STL idiom
}

/*****************************************************************************
 * Button wrapper.
 *****************************************************************************/
GuiButton*
GuiScreen::NewButton( float x, float y, eFontMode fontMode, fnMouseButtonCallback callback )
{
	return new GuiButton( *this, x, y, fontMode, callback );
}
void
GuiScreen::DeleteButton( GuiButton* button )
{
	delete button;
}

////////////////////////////////////////////////////////////////////////////////
/////////////////////////////  GuiObject class  ////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * Constructor.
 *****************************************************************************/
GuiObject::GuiObject( GuiScreen& screen, float x, float y, fnMouseButtonCallback buttonCallback )
:	mGui(screen.mGui),
	mScreen(screen),
	mType(eGuiObject_BASE),
	mVisible(false),
	mSensitive(false),
	mMouseButtonCallback(buttonCallback),
	mMouseEvent(eMouseEvent_NONE),
	mPos(x,y),
    mRect(0,0,0,0),     // zero thickness, ignored by mouse handler
    mLinkObject(NULL)   // assigned below...
{
	// mLinkObject (implicit):
	screen.LinkObject( this );      // add to screen's list of objects
}
GuiObject::~GuiObject()
{
//	mScreen.UnlinkObject( this );	// ~GuiScreen()'s delete objectList will unlink this!
}

/*****************************************************************************
 * Set/get object position.
 *****************************************************************************/
void
GuiObject::GetPos( float& x /*OUT*/, float& y /*OUT*/ )
{
	x = mPos.x;
	y = mPos.y;
}
void
GuiObject::SetPos( float x, float y )
{
	mPos.x = x;
	mPos.y = y;
}

////////////////////////////////////////////////////////////////////////////////
//////////////////////////////  GuiPrint class  ////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * GuiPrint class.  Used for messages that expire, not for regular Print().
 *****************************************************************************/
GuiPrint::GuiPrint( GuiScreen& screen, int seconds, const PrintDesc& desc )
:   GuiObject( screen, desc.mPos.x, desc.mPos.y, NULL )
{
	// GuiObject:
	mType		= eGuiObject_PRINT;
	mMouseButtonCallback = NULL;
	// GuiPrint:
	mText       = desc.mText;
	mTimer		= seconds;
	mRgbaBase   = desc.mRgba;
	mFontMode	= desc.mFontMode;
	mFontType	= desc.mFontType;
	mFontSize	= desc.mFontSize;
	mFont		= mGui.GetFont( desc.mFontType );
}
GuiPrint::~GuiPrint()
{
	// nothing
}
void
GuiPrint::Render( void )
{
	// The fact this object exists and this point was reached
	// means its timer hasn't expired.
	mScreen.PushPrint( mFontType, mFontSize );
	mFont->Print( mText, mPos.x, mPos.y, mFontMode, mRgbaBase );
	mScreen.PopPrint();
}

////////////////////////////////////////////////////////////////////////////////
///////////////////////////  GuiButtonBase class  //////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * Constructor.
 *****************************************************************************/
GuiButtonBase::GuiButtonBase( GuiScreen& screen, float x, float y, eFontMode fontMode, fnMouseButtonCallback callback )
:   GuiObject(screen,x,y,callback)
{
	// GuiObject:
	mType		= eGuiObject_BUTTON;
	mMouseButtonCallback = callback;
	// GuiButtonBase:
	mFontMode	= fontMode;
	//mText		= "";
	mRgbaBase   = sRgbaDefault;
}
GuiButtonBase::~GuiButtonBase()
{
	// NOP
}

/*****************************************************************************
 * Set button text.
 *****************************************************************************/
void
GuiButtonBase::SetText( const char* text, uint textLen )
{
	mText.assign( text, textLen );
	GFX_POST_REDISPLAY();
}
void
GuiButtonBase::SetText( const string& text )
{
	mText = text;
	GFX_POST_REDISPLAY();
}

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////  Gfont  ///////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * Gfont ctor/dtor.
 *****************************************************************************/
Gfont::Gfont( FontPtrc font, eScreenFontSize size, float zoom, const RGBA rgba )
:   mFont(font),
    mSize(size),
    mZoom(zoom),
    mRgba(rgba)
{
    // NOP
}
Gfont::~Gfont()
{
    // NOP
}

/*****************************************************************************
 * Gfont methods.
 *****************************************************************************/
void
Gfont::SetZoom( float zoom )
{
    mZoom = zoom;
    mFont->SetZoom( zoom );  // zoom underlying Font object too
}
int
Gfont::Print( const char* text, uint textLen, float x, float y, eFontMode fontMode )
{
    return mFont->Print( text, textLen, x, y, fontMode );
}
int
Gfont::Print( const char* text, uint textLen, float x, float y, eFontMode fontMode, const RGBA rgba )
{
    return mFont->Print( text, textLen, x, y, fontMode, rgba );
}

////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////  PrintDesc  /////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

/*****************************************************************************
 * PrintDesc.
 *****************************************************************************/
PrintDesc::PrintDesc( void )
: //mText(""),
	mPos(0,0),
	mRgba(sRgbaDefault),
	mFontMode(eFontMode_CENTER),
	mFontType(eScreenFontType_NORMAL),
	mFontSize(eScreenFontSize_NORMAL)
{
}

} // namespace gfx
