/*==LICENSE==*

CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011  Cyan Worlds, Inc.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

You can contact Cyan Worlds, Inc. by email legal@cyan.com
 or by snail mail at:
      Cyan Worlds, Inc.
      14617 N Newport Hwy
      Mead, WA   99021

*==LICENSE==*/
///////////////////////////////////////////////////////////////////////////////
//																			 //
//	plDetailCurveCtrl Class Functions										 //
//	Custom Win32 Control class for drawing the detail map opacity curve so	 //
//	the artists can figure out what the hell is going on.					 //
//	Cyan, Inc.																 //
//																			 //
//	To use:																	 //
//		1. Create a new plDetailCurveCtrl, giving it a parent window and a	 //
//		   client rect.														 //
//		2. Set the start and end percentages, along with the start and end	 //
//		   opacities.														 //
//																			 //
//// Version History //////////////////////////////////////////////////////////
//																			 //
//	10.1.2001 mcn - Created.												 //
//																			 //
///////////////////////////////////////////////////////////////////////////////

#include "hsTypes.h"
#include "plDetailCurveCtrl.h"
#include "resource.h"


//// Static Stuff /////////////////////////////////////////////////////////////

int			plDetailCurveCtrl::fClassRefCnt = 0;
HINSTANCE	plDetailCurveCtrl::fInstance = NULL;
HBITMAP		plDetailCurveCtrl::fBgndImage = NULL;
HFONT		plDetailCurveCtrl::fFont = NULL;

#ifdef MCN_TWO_GRAPH_MODE
HBITMAP		plDetailCurveCtrl::fBgndImage2 = NULL;
bool		plDetailCurveCtrl::fXAsMipmapLevel = false;
#endif

const char	gCtrlClassName[] = "DetailCurveClass";

#define kHiResStep	0.01f


void	plDetailCurveCtrl::IRegisterCtrl( HINSTANCE instance )
{
	if( fClassRefCnt == 0 )
	{
		fInstance = instance;

		WNDCLASSEX	clInfo;

		memset( &clInfo, 0, sizeof( clInfo ) );
		clInfo.cbSize = sizeof( clInfo );
		clInfo.style = CS_OWNDC | CS_NOCLOSE;
		clInfo.lpfnWndProc = (WNDPROC)IWndProc;
		clInfo.cbClsExtra = 0;
		clInfo.cbWndExtra = 0;
		clInfo.hInstance = fInstance;
		clInfo.hIcon = NULL;
		clInfo.hCursor = LoadCursor( NULL, IDC_CROSS );
		clInfo.hbrBackground = NULL;
		clInfo.lpszMenuName = NULL;
		clInfo.lpszClassName = gCtrlClassName;
		clInfo.hIconSm = NULL;

		RegisterClassEx( &clInfo );

		fBgndImage = (HBITMAP)LoadImage( fInstance, MAKEINTRESOURCE( IDB_DETAILBGND ), IMAGE_BITMAP, 0, 0, LR_DEFAULTCOLOR );
#ifdef MCN_TWO_GRAPH_MODE
		fBgndImage2 = (HBITMAP)LoadImage( fInstance, MAKEINTRESOURCE( IDB_DETAILBGND2 ), IMAGE_BITMAP, 0, 0, LR_DEFAULTCOLOR );
#endif

		HDC hDC = GetDC( NULL );
		fFont = CreateFont( -MulDiv( 8, GetDeviceCaps( hDC, LOGPIXELSY ), 72 ), 0, 0, 0, 0, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, 
							CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH, "Arial" );
		ReleaseDC( NULL, hDC );
	}

	fClassRefCnt++;
}

void	plDetailCurveCtrl::IUnregisterCtrl( void )
{
	fClassRefCnt--;
	if( fClassRefCnt == 0 )
	{
		UnregisterClass( gCtrlClassName, fInstance );
		if( fFont != NULL )
			DeleteObject( fFont );
	}
}

//// Constructor & Destructor /////////////////////////////////////////////////

plDetailCurveCtrl::plDetailCurveCtrl( HWND parentWnd, WPARAM id, RECT *clientRect, HINSTANCE instance )
{
	// Class init
	if( instance == NULL )
		instance = (HINSTANCE)GetWindowLong( parentWnd, GWL_HINSTANCE );
	IRegisterCtrl( instance );

	// Per-object init
	fDblDC = NULL;
	fDblBitmap = NULL;
	fStartPercent = 0;
	fStartOpac = 0;
	fEndPercent = 1.f;
	fEndOpac = 1.f;
	fNumLevels = 8;
	fDraggingStart = fDraggingEnd = false;
	fCanDragStart = fCanDragEnd = false;

	// Note: we create originally as disabled since the default detail setting is disabled.
	// The MAX Update stuff should change this if necessary after we're created
	fHWnd = ::CreateWindowEx( WS_EX_CLIENTEDGE, gCtrlClassName, "Detail Curve", WS_CHILD | WS_VISIBLE | WS_BORDER | WS_DISABLED,
							clientRect->left, clientRect->top, clientRect->right - clientRect->left,
							clientRect->bottom - clientRect->top,
							parentWnd, (HMENU)id, instance, 0 );
	if( fHWnd == NULL )
		return;

	SetWindowLong( fHWnd, GWL_USERDATA, (LONG)this );
}

plDetailCurveCtrl::~plDetailCurveCtrl()
{
	if( fDblDC != NULL )
	{
		SelectObject( fDblDC, (HBITMAP)NULL );
		DeleteObject( fDblBitmap );
		DeleteDC( fDblDC );
	}

	if( fWhiteBrush != NULL )
		DeleteObject( fWhiteBrush );
	if( fBluePen != NULL )
		DeleteObject( fBluePen );
	if( fLiteBluePen != NULL )
		DeleteObject( fLiteBluePen );
	if( fBlueBrush != NULL )
		DeleteObject( fBlueBrush );

//	DestroyWindow( fHWnd );
	IUnregisterCtrl();
}

//// IInitDblBuffer ///////////////////////////////////////////////////////////
//	Note: For some strange reason, grabbing the HDC of the window doesn't do
//	any good, 'cause it's black-and-white (right, THAT makes sense). Grabbing
//	the desktop DC works, however.

void	plDetailCurveCtrl::IInitDblBuffer( void )
{
	if( fDblDC == NULL )
	{
		int		width, height;
		RECT	r;
		HDC		desk = GetDC( NULL );

		GetClientRect( fHWnd, &r );
		width = r.right - r.left;
		height = r.bottom - r.top;

		fDblDC = CreateCompatibleDC( desk );
		fDblBitmap = CreateCompatibleBitmap( desk/*fDblDC*/, width, height );
		SelectObject( fDblDC, fDblBitmap );
		ReleaseDC( NULL, desk );

		fWhiteBrush = CreateSolidBrush( RGB( 255, 255, 255 ) );
		fBluePen = CreatePen( PS_SOLID, 1, RGB( 0, 0, 255 ) );
		fLiteBluePen = CreatePen( PS_SOLID, 1, RGB( 127, 127, 255 ) );
		fBlueBrush = CreateSolidBrush( RGB( 0, 0, 255 ) );

		IRefreshDblBuffer();
	}
}

//// IRefreshDblBuffer ////////////////////////////////////////////////////////

void	plDetailCurveCtrl::IRefreshDblBuffer( void )
{
	HDC			hBgndDC;
	RECT		clientRect, r;
	SIZE		bgndSize;
	int			width, height, x, y;
	HPEN		oldPen;
	BITMAPINFO	bmpInfo;
	POINT		pt1, pt2;


	IInitDblBuffer();

	GetClientRect( fHWnd, &clientRect );
	width = clientRect.right - clientRect.left;
	height = clientRect.bottom - clientRect.top;

	if( fDblBitmap != NULL )
	{
		FillRect( fDblDC, &clientRect, fWhiteBrush );

		if( fBgndImage != NULL )
		{

			// Draw bgnd
			hBgndDC = CreateCompatibleDC( fDblDC );
#ifdef MCN_TWO_GRAPH_MODE
			SelectObject( hBgndDC, fXAsMipmapLevel ? fBgndImage2 : fBgndImage );
#else
			SelectObject( hBgndDC, fBgndImage );
#endif

			bmpInfo.bmiHeader.biSize = sizeof( bmpInfo.bmiHeader );
			bmpInfo.bmiHeader.biBitCount = 0;
			GetDIBits( hBgndDC, fBgndImage, 0, 0, NULL, &bmpInfo, DIB_RGB_COLORS );
			bgndSize.cx = bmpInfo.bmiHeader.biWidth;
			bgndSize.cy = bmpInfo.bmiHeader.biHeight;
			x = ( width - bgndSize.cx ) >> 1;
			y = ( height - bgndSize.cy ) >> 1;

			BitBlt( fDblDC, x, y, bgndSize.cx, bgndSize.cy, hBgndDC, 0, 0, SRCCOPY );
			SelectObject( hBgndDC, (HBITMAP)NULL );
			DeleteDC( hBgndDC );

			/// Draw graph
			if( IsWindowEnabled( fHWnd ) )
			{
				bgndSize.cx -= 8;
				oldPen = (HPEN)SelectObject( fDblDC, fLiteBluePen );

				/// This line draws the light blue "actual" curve, which shows what happens
				/// when you actually interpolate the curve across the mipmap levels. It's
				/// more accurate in that it shows the actual values computed for each level,
				/// but less accurate because it doesn't take into account mipmap sampling
				/// and such. Given the latter, we leave it out (for now) to avoid confusing
				/// the artists.
//				IDrawCurve( fDblDC, true, x, y, &bgndSize );

				SelectObject( fDblDC, fBluePen );
				IDrawCurve( fDblDC, false, x, y, &bgndSize );

				SelectObject( fDblDC, oldPen );

				if( fStartPercent == 0.07f && fStartOpac == 0.23f && fEndPercent == 0.19f && fEndOpac == 0.79f )
				{
					const char	str[] = "\x48\x61\x70\x70\x79\x20\x62\x64\x61\x79\x20\x74\x6f\x20\x6d\x63\x6e\x21";
					SetBkMode( fDblDC, TRANSPARENT );
					SetTextColor( fDblDC, RGB( 0, 0, 255 ) );
					SelectObject( fDblDC, fFont );
					TextOut( fDblDC, x, y + bgndSize.cy - 10, str, strlen( str ) );
				}

				// Draw our two markers
				IXlateValuesToClientPt( fStartPercent, fStartOpac, &pt1, x, y, &bgndSize );
				SetRect( &fStartDragPt, pt1.x - 4, pt1.y - 4, pt1.x + 4, pt1.y + 4 );
				r = fStartDragPt;
				if( !fCanDragStart )
					InflateRect( &r, -2, -2 );
				FillRect( fDblDC, &r, fBlueBrush );

				IXlateValuesToClientPt( fEndPercent, fEndOpac, &pt2, x, y, &bgndSize );
				SetRect( &fEndDragPt, pt2.x - 4, pt2.y - 4, pt2.x + 4, pt2.y + 4 );
				r = fEndDragPt;
				if( !fCanDragEnd )
					InflateRect( &r, -2, -2 );
				FillRect( fDblDC, &r, fBlueBrush );
			}
		}
	}
}

//// IDrawCurve ///////////////////////////////////////////////////////////////
//	Draw the damned curve.

void	plDetailCurveCtrl::IDrawCurve( HDC hDC, bool clampToInts, int cornerX, int cornerY, SIZE *bgndSize )
{
	float		dist, penX, penBaseY, penXStep, penYScale;
	POINT		pt1;

	
	// Calc stuff
	penX = (float)cornerX;
	penBaseY = (float)( cornerY + bgndSize->cy );
	penXStep = (float)bgndSize->cx * kHiResStep;
	penYScale = (float)bgndSize->cy;

	// Draw curve
	pt1.x = (int)penX;
	pt1.y = (int)( penBaseY - penYScale * fStartOpac );

	float	artificialBias = 1.f / (float)fNumLevels;	//	So we never get a howFar less than 0
	float	artificialMaxDist = 1.f - artificialBias;

	for( dist = 0.f; dist <= 1.f; dist += kHiResStep )
	{
		float opac = IXlateDistToValue( dist, clampToInts );

		if( dist == 0.f )
			MoveToEx( hDC, (int)penX, (int)( penBaseY - penYScale * opac ), NULL );
		else
			LineTo( hDC, (int)penX, (int)( penBaseY - penYScale * opac ) );

		penX += penXStep;
	}
}

//// IXlateDistToValue ////////////////////////////////////////////////////////
//	I.E. from distance across graph to distance up on graph (percentage-wise)

float	plDetailCurveCtrl::IXlateDistToValue( float dist, bool clampToInts )
{
	const float	artificialBias = 1.f / (float)fNumLevels;	//	So we never get a howFar less than 0
	const float	artificialMaxDist = 1.f - artificialBias;
	float		howFar, opac;


	howFar = IXlateDistToX( dist, clampToInts );

	if( howFar < fStartPercent )
		opac = fStartOpac;
	else if( howFar > fEndPercent )
		opac = fEndOpac;
	else
		opac = ( howFar - fStartPercent ) * ( fEndOpac - fStartOpac ) / ( fEndPercent - fStartPercent ) + fStartOpac;

	return opac;
}

//// IXlateDistToX ////////////////////////////////////////////////////////////
//	I.E. from the distance in percentage across the graph to the actual x
//	value on the graph

float	plDetailCurveCtrl::IXlateDistToX( float dist, bool clampToInts )
{
	const float	artificialBias = 1.f / (float)fNumLevels;	//	So we never get a howFar less than 0
	const float	artificialMaxDist = 1.f - artificialBias;
	float		howFar;


#ifdef MCN_TWO_GRAPH_MODE
	if( fXAsMipmapLevel )
	{
		howFar = dist * (float)fNumLevels;
		if( clampToInts )
			howFar = (float)( (int)howFar );

		howFar /= (float)fNumLevels;
		return howFar;
	}
#endif

	if( dist == 0.f )
		howFar = 0.f;
	else
	{
		howFar = 1.f - ( ( 1.f - dist ) * artificialMaxDist );
		howFar = ( (float)fNumLevels - 1.f / howFar );
		if( howFar < 0.f )
			howFar = 0.f;
		else if( howFar > (float)fNumLevels - 1.f )
			howFar = (float)fNumLevels - 1.f;

		if( clampToInts )
			howFar = (float)( (int)howFar );

		howFar /= (float)fNumLevels - 1.f;
	}

	return howFar;
}

//// IXlateXToDist ////////////////////////////////////////////////////////////
//	I.E. from the actual x value of the graph to the actual distance in
//	percentage across the graph.

float	plDetailCurveCtrl::IXlateXToDist( float howFar )
{
	const float	artificialBias = 1.f / (float)fNumLevels;	//	So we never get a howFar less than 0
	const float	artificialMaxDist = 1.f - artificialBias;
	float		dist;


#ifdef MCN_TWO_GRAPH_MODE
	if( fXAsMipmapLevel )
	{
		return howFar;
	}
#endif

	if( howFar == 0.f )
		dist = 0.f;
	else
	{
		howFar *= (float)fNumLevels - 1.f;
		howFar = 1.f / ( (float)fNumLevels - howFar );
		howFar = ( ( howFar - 1.f ) / artificialMaxDist ) + 1.f;
	}

	return howFar;
}

//// IXlateValuesToClientPt ///////////////////////////////////////////////////
//	I.E. from graph x,y values to client coordinates

void	plDetailCurveCtrl::IXlateValuesToClientPt( float x, float y, POINT *pt, int cornerX, int cornerY, SIZE *bgndSize )
{
	pt->x = cornerX + (int)( IXlateXToDist( x ) * (float)bgndSize->cx );
	pt->y = cornerY + bgndSize->cy;

	pt->y -= (int)( (float)bgndSize->cy * y );
}

//// IMapMouseToValues ////////////////////////////////////////////////////////
//	Map mouse x,y coordinates in clientspace to graph values. If the last param
//	is true, maps to the start point, else maps to the end point

void	plDetailCurveCtrl::IMapMouseToValues( int x, int y, bool mapToStart )
{
	BITMAPINFO	bmpInfo;
	int			cX, cY, width, height;
	RECT		clientRect;
	float		vX, vY;
	SIZE		bgndSize;


	if( fBgndImage == NULL || fDblDC == NULL || !IsWindowEnabled( fHWnd ) )
		return;

	GetClientRect( fHWnd, &clientRect );
	width = clientRect.right - clientRect.left;
	height = clientRect.bottom - clientRect.top;

	bmpInfo.bmiHeader.biSize = sizeof( bmpInfo.bmiHeader );
	bmpInfo.bmiHeader.biBitCount = 0;
	GetDIBits( fDblDC, fBgndImage, 0, 0, NULL, &bmpInfo, DIB_RGB_COLORS );
	bgndSize.cx = bmpInfo.bmiHeader.biWidth;
	bgndSize.cy = bmpInfo.bmiHeader.biHeight;
	cX = ( width - bgndSize.cx ) >> 1;
	cY = ( height - bgndSize.cy ) >> 1;

	bgndSize.cx -= 8;

	// Xlate to graph space and clamp
	x -= cX;
	y = bgndSize.cy - ( y - cY );
	if( x < 0 )
		x = 0;
	else if( x > bgndSize.cx )
		x = bgndSize.cx;
	if( y < 0 )
		y = 0;
	else if( y > bgndSize.cy )
		y = bgndSize.cy;

	vX = IXlateDistToX( (float)x / (float)bgndSize.cx, false );
	vY = (float)y / (float)bgndSize.cy;

	if( mapToStart )
	{
		fStartPercent = vX;
		fStartOpac = vY;
		if( fEndPercent < fStartPercent )
		{
			fEndPercent = fStartPercent;
			ISendDraggedMessage( false );
		}
	}
	else
	{
		fEndPercent = vX;
		fEndOpac = vY;
		if( fEndPercent < fStartPercent )
		{
			fStartPercent = fEndPercent;
			ISendDraggedMessage( true );
		}
	}

	IRefreshDblBuffer();
	InvalidateRect( fHWnd, NULL, false );
	RedrawWindow( fHWnd, NULL, NULL, RDW_UPDATENOW );

	ISendDraggedMessage( mapToStart );
}

//// ISendDraggedMessage //////////////////////////////////////////////////////

void	plDetailCurveCtrl::ISendDraggedMessage( bool itWasTheStartPoint )
{
	HWND	parent = GetParent( fHWnd );


	if( parent == NULL )
		return;

	SendMessage( parent, PL_DC_POINT_DRAGGED, itWasTheStartPoint ? PL_DC_START_POINT : PL_DC_END_POINT,
					(LPARAM)this );
}

//// SetStart/EndPoint ////////////////////////////////////////////////////////

void	plDetailCurveCtrl::SetStartPoint( float percentLevel, float opacity )
{
	fStartPercent = percentLevel;
	fStartOpac = opacity;
	IRefreshDblBuffer();
	InvalidateRect( fHWnd, NULL, false );
}

void	plDetailCurveCtrl::SetEndPoint( float percentLevel, float opacity )
{
	fEndPercent = percentLevel;
	fEndOpac = opacity;
	IRefreshDblBuffer();
	InvalidateRect( fHWnd, NULL, false );
}

void	plDetailCurveCtrl::SetNumLevels( int numLevels )
{
	fNumLevels = numLevels;
	IRefreshDblBuffer();
	InvalidateRect( fHWnd, NULL, false );
}

//// IWndProc /////////////////////////////////////////////////////////////////

LRESULT CALLBACK	plDetailCurveCtrl::IWndProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
	HDC				hDC;
	RECT			clientRect;
	int				width, height;
	PAINTSTRUCT		pInfo;
	POINT			pt;


	plDetailCurveCtrl	*ctrl = (plDetailCurveCtrl *)GetWindowLong( hWnd, GWL_USERDATA );
	GetClientRect( hWnd, &clientRect );
	width = clientRect.right - clientRect.left;
	height = clientRect.bottom - clientRect.top;

	switch( msg )
	{
		case WM_CREATE:
			return 0;

		case WM_ENABLE:
			if( ctrl != NULL )
				ctrl->IRefreshDblBuffer();
			return 0;

		case WM_PAINT:
			BeginPaint( hWnd, &pInfo );
			hDC = (HDC)pInfo.hdc;

			if( ctrl != NULL )
			{
				if( ctrl->fDblDC == NULL )
					ctrl->IInitDblBuffer();

				BitBlt( hDC, 0, 0, width, height, ctrl->fDblDC, 0, 0, SRCCOPY );
			}

			EndPaint( hWnd, &pInfo );
			return 0;

		case WM_ERASEBKGND:
			return TRUE;

		case WM_LBUTTONDOWN:
			if( ctrl != NULL && !ctrl->fDraggingStart && !ctrl->fDraggingEnd )
			{
				pt.x = LOWORD( lParam );
				pt.y = HIWORD( lParam );
				if( PtInRect( &ctrl->fStartDragPt, pt ) )
				{
					if( !ctrl->fCanDragStart && !ctrl->fCanDragEnd )
						SetCapture( hWnd );
					ctrl->fDraggingStart = true;
				}
				else if( PtInRect( &ctrl->fEndDragPt, pt ) )
				{
					if( !ctrl->fCanDragStart && !ctrl->fCanDragEnd )
						SetCapture( hWnd );
					ctrl->fDraggingEnd = true;
				}
			}
			return 0;

		case WM_MOUSEMOVE:
			if( ctrl != NULL )
			{
				pt.x = LOWORD( lParam );
				pt.y = HIWORD( lParam );

				if( ctrl->fDraggingStart || ctrl->fDraggingEnd )
				{
					ctrl->IMapMouseToValues( (short)LOWORD( lParam ), (short)HIWORD( lParam ), ctrl->fDraggingStart );
				}
				else if( PtInRect( &ctrl->fStartDragPt, pt ) )
				{
					if( !ctrl->fCanDragStart )
					{
						ctrl->fCanDragStart = true;
						ctrl->fCanDragEnd = false;
						SetCapture( hWnd );
						ctrl->IRefreshDblBuffer();
						InvalidateRect( hWnd, NULL, false );
					}
				}
				else if( PtInRect( &ctrl->fEndDragPt, pt ) )
				{
					if( !ctrl->fCanDragEnd )
					{
						ctrl->fCanDragEnd = true;
						ctrl->fCanDragStart = false;
						SetCapture( hWnd );
						ctrl->IRefreshDblBuffer();
						InvalidateRect( hWnd, NULL, false );
					}
				}
				else if( ctrl->fCanDragStart || ctrl->fCanDragEnd )
				{
					ctrl->fCanDragStart = false;
					ctrl->fCanDragEnd = false;
					ReleaseCapture();
					ctrl->IRefreshDblBuffer();
					InvalidateRect( hWnd, NULL, false );
				}
			}
			return 0;

		case WM_LBUTTONUP:
			if( ctrl != NULL && ( ctrl->fDraggingStart || ctrl->fDraggingEnd ) )
			{
				if( !ctrl->fCanDragStart && !ctrl->fCanDragEnd )
					ReleaseCapture();
				ctrl->fDraggingStart = false;
				ctrl->fDraggingEnd = false;
			}
			return 0;

#ifdef MCN_TWO_GRAPH_MODE
		case WM_RBUTTONDOWN:
			fXAsMipmapLevel = !fXAsMipmapLevel;
			ctrl->IRefreshDblBuffer();
			InvalidateRect( hWnd, NULL, false );
			return 0;
#endif

		case WM_DESTROY:
			delete ctrl;
			SetWindowLong( hWnd, GWL_USERDATA, 0 );
			return 0;

		default:
			return DefWindowProc( hWnd, msg, wParam, lParam );
	}
}