// NeL - MMORPG Framework <http://dev.ryzom.com/projects/nel/>
// Copyright (C) 2010  Winch Gate Property Limited
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

//

#include "std_afx.h"
#include "object_viewer.h"
#include "curve_edit.h"
#include "editable_range.h"
#include "nel/3d/ps_float.h"
#include "nel/misc/common.h"
#include "nel/misc/fast_floor.h"

static const uint CtrlPointSize = 3;



CurveEdit::CurveEdit(NL3D::CPSFloatCurveFunctor *curve, CParticleWorkspace::CNode *ownerNode, IPopupNotify *pn, CWnd* pParent /*=NULL*/)
	: CDialog(CurveEdit::IDD, pParent), 
	  _Node(ownerNode),
	  _PN(pn),
	  Curve(curve),
	  _State(Create),
	  _X(10),
	  _Y(10),
	  _Width(350),
	  _Height(200),
	  _SelectedCtrlPoint(-1),
	  _NumSamplesDlg(NULL)

{
	nlassert(Curve);	
	//{{AFX_DATA_INIT(CurveEdit)
	m_DisplayInterpolation = FALSE;
	m_SmoothingOn = FALSE;
	//}}AFX_DATA_INIT
	scaleMinMax();	
}

CurveEdit::~CurveEdit()
{
	if (_NumSamplesDlg)
	{
		_NumSamplesDlg->DestroyWindow();
		delete _NumSamplesDlg;
	}
}


void CurveEdit::DoDataExchange(CDataExchange* pDX)
{
	CDialog::DoDataExchange(pDX);
	//{{AFX_DATA_MAP(CurveEdit)
	DDX_Check(pDX, IDC_DISPLAY_INTERPOLATION, m_DisplayInterpolation);
	DDX_Check(pDX, IDC_SMOOTHING_ON, m_SmoothingOn);
	//}}AFX_DATA_MAP
}


BEGIN_MESSAGE_MAP(CurveEdit, CDialog)
	//{{AFX_MSG_MAP(CurveEdit)
	ON_WM_MOUSEMOVE()
	ON_WM_LBUTTONUP()
	ON_WM_LBUTTONDOWN()
	ON_WM_PAINT()
	ON_BN_CLICKED(IDC_ZOOM_OUT, OnZoomOut)
	ON_BN_CLICKED(IDC_ZOOM_IN, OnZoomIn)
	ON_BN_CLICKED(IDC_GO_UP, OnGoUp)
	ON_BN_CLICKED(IDC_GO_DOWN, OnGoDown)
	ON_BN_CLICKED(IDC_MOVE_POINT, OnMovePoint)
	ON_BN_CLICKED(IDC_ADD_POINT, OnAddPoint)
	ON_BN_CLICKED(IDC_REMOVE_POINT, OnRemovePoint)
	ON_BN_CLICKED(IDC_DISPLAY_INTERPOLATION, OnDisplayInterpolation)
	ON_BN_CLICKED(IDC_SMOOTHING_ON, OnSmoothingOn)
	ON_BN_CLICKED(IDC_LAST_EQUAL_FIRST, OnLastEqualFirst)
	ON_BN_CLICKED(IDC_CENTER_CURVE, OnCenterCurve)
	ON_BN_CLICKED(IDC_FIRST_EQUAL_LAST, OnFirstEqualLast)
	ON_WM_DESTROY()
	ON_WM_CLOSE()
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CurveEdit message handlers

void CurveEdit::OnMouseMove(UINT nFlags, CPoint point) 
{
	switch (_State)
	{
		case Move:
		case Remove:
		{
			sint cpIndex = intersectCtrlPoint(point.x, point.y);
			if (cpIndex != _SelectedCtrlPoint)
			{
				_SelectedCtrlPoint = cpIndex;	
				Invalidate();
			}			
		}
		break;
		case Moving:
		{
			Curve->setCtrlPoint(_SelectedCtrlPoint,coordsFromScreen(point.x, point.y));
			_SelectedCtrlPoint = intersectCtrlPoint(point.x, point.y); // the index of the point we are moving may have changed
			if (_SelectedCtrlPoint == -1) _State = Move;
			Invalidate();
		}
		break;		
	}
	
	CDialog::OnMouseMove(nFlags, point);
}

void CurveEdit::OnLButtonUp(UINT nFlags, CPoint point) 
{
	switch (_State)
	{
		case Moving:
			_State = Move;
			_SelectedCtrlPoint = intersectCtrlPoint(point.x, point.y);
			Invalidate();
		break;
		case Removing:
			_State = Remove;
		break;
		case Created:
			_State = Create;
		break;
	}
	CDialog::OnLButtonUp(nFlags, point);
}

void CurveEdit::OnLButtonDown(UINT nFlags, CPoint point) 
{
	switch (_State)
	{
		case Move:
		{
			if (_SelectedCtrlPoint != -1) _State = Moving;
		}
		break;
		case Create:
		{
			Curve->addControlPoint(coordsFromScreen(point.x, point.y));
			_State = Created;
			invalidate();
		}
		case Remove:
		{
			if ( _SelectedCtrlPoint == -1 || Curve->getNumCtrlPoints() == 2 ) return;
			Curve->removeCtrlPoint(_SelectedCtrlPoint);
			_State= Removing;
			_SelectedCtrlPoint = -1;
			invalidate();
		}
		break;
	}
	CDialog::OnLButtonDown(nFlags, point);
}

void CurveEdit::OnPaint() 
{
	UpdateData();
	CPaintDC dc(this); // device context for painting	
	drawBackGround(dc);
	drawUnits(dc);
	drawCurve(dc);
	if (m_DisplayInterpolation) drawInterpolatingCurve(dc);
	drawCtrlPoints(dc);	
}


void CurveEdit::setupBoundRect(CDC &dc)
{	
	CRgn rgn;
	rgn.CreateRectRgn(_X, _X, _X + _Width, _Y + _Height);
	dc.SelectClipRgn(&rgn, RGN_COPY  );	
}

void CurveEdit::drawBackGround(CDC &dc)
{
	setupBoundRect(dc);	
	dc.FillSolidRect(_X, _Y,  _Width, _Height, 0xffffff);
}



POINT CurveEdit::makePoint(float date, float value) const
{
	POINT p;
	p.x = (int) (_X + date * _Width);
	p.y = (int) (_Y + _Height - _Scale * _Height * (value - _Origin));
	return p;
}


sint CurveEdit::intersectCtrlPoint(sint x, sint y)
{
	uint numPoints = Curve->getNumCtrlPoints();
	for (uint k = 0; k < numPoints; ++k)
	{
		const CCtrlPoint &cp = Curve->getControlPoint(k);
		POINT p = makePoint(cp.Date, cp.Value);
		if (   x >= (sint) (p.x - CtrlPointSize) 
			&& x <= (sint) (p.x + CtrlPointSize)
			&& y >= (sint) (p.y - CtrlPointSize) 
			&& y <= (sint) (p.y + CtrlPointSize) )
		{
			return k;
		}
	}
	return -1;
}

CurveEdit::CCtrlPoint CurveEdit::coordsFromScreen(sint x, sint y) const
{
	float date =(x - _X) / (float) _Width;
	NLMISC::clamp(date, 0.f, 1.f);
	CCtrlPoint pos(
		             date,
					 _Origin + (_Y + _Height - y) / (_Scale * _Height)
				  );	
	return pos;
}


void CurveEdit::drawCurve(CDC &dc)
{
setupBoundRect(dc);	
CPen pen;
pen.CreatePen(PS_SOLID, 1, (COLORREF) 0x000000);
CGdiObject *oldPen = dc.SelectObject(&pen);

	dc.MoveTo(makePoint(0, Curve->getValue(0)));
	for (sint x = 0; x < _Width; ++x)
	{
		const float date = x / (float) _Width;
		dc.LineTo(_X + x, makePoint(date, Curve->getValue(date)).y);
	}
dc.SelectObject(oldPen);
}


void CurveEdit::drawInterpolatingCurve(CDC &dc)
{
setupBoundRect(dc);	
CPen pen;
pen.CreatePen(PS_SOLID, 1, (COLORREF) 0x772211);
CGdiObject *oldPen = dc.SelectObject(&pen);
dc.MoveTo(makePoint(0, getSampledValue(0)));
for (sint x = 0; x < _Width; ++x)
{
	const float date = x / (float) _Width;
	dc.LineTo(_X + x, makePoint(date, getSampledValue(date)).y);
}
dc.SelectObject(oldPen);
}



void CurveEdit::scaleMinMax()
{
	float minValue, maxValue;
	maxValue = minValue = getSampledValue(0);
	for (sint x = 1; x < _Width; ++x)
	{
		const float date = x / (float) _Width;
		const float value = getSampledValue(date);
		minValue = std::min(minValue, value);
		maxValue = std::max(maxValue, value);
	}
	_Origin = (maxValue == minValue) ? minValue - .5f : minValue;
	_Scale = (maxValue == minValue) ? 1.f : 1.f / (maxValue - minValue);
}




void CurveEdit::drawCtrlPoints(CDC &dc)
{
	setupBoundRect(dc);	
	CPen pens[2];
	pens[0].CreatePen(PS_SOLID, 1, (COLORREF) 0x00ff00);
	pens[1].CreatePen(PS_SOLID, 1, (COLORREF) 0x0000ff);

	uint numPoints = Curve->getNumCtrlPoints();
	for (uint k = 0; k < numPoints; ++k)
	{
		CGdiObject *oldPen = dc.SelectObject(&pens[k == (uint) _SelectedCtrlPoint ? 1 : 0]); // slect the red pen if thi ctrl point is selected
		const CCtrlPoint &cp = Curve->getControlPoint(k);
		POINT p = makePoint(cp.Date, cp.Value);
		dc.MoveTo(p.x- CtrlPointSize, p.y - CtrlPointSize);
		dc.LineTo(p.x + CtrlPointSize, p.y - CtrlPointSize);
		dc.LineTo(p.x + CtrlPointSize, p.y + CtrlPointSize);
		dc.LineTo(p.x - CtrlPointSize, p.y + CtrlPointSize);
		dc.LineTo(p.x - CtrlPointSize, p.y - CtrlPointSize);
		dc.SelectObject(oldPen);
	}
}


void CurveEdit::OnZoomOut() 
{
	_Scale *= 1.5f;
	Invalidate();	
}

void CurveEdit::OnZoomIn() 
{
	_Scale *= 0.5f;
	Invalidate();		
}

void CurveEdit::OnGoUp() 
{
	_Origin += (1.f / _Scale) * 0.25f;
	Invalidate();		
}

void CurveEdit::OnGoDown() 
{
	_Origin -= (1.f / _Scale) * 0.25f;	
	Invalidate();		
}


void CurveEdit::drawUnits(CDC &dc)
{
	CPen pens[2];
	pens[0].CreatePen(PS_SOLID, 1, (COLORREF) 0xaaaaaa);
	pens[1].CreatePen(PS_SOLID, 1, (COLORREF) 0xff0011);

	sint upVal = (sint) floorf(coordsFromScreen(0, _X).Value);
	sint downVal = (sint) floorf(coordsFromScreen(0, _X + _Width).Value);

	nlassert(upVal >= downVal);
	for (sint k = downVal ; k <= upVal ; ++k)
	{
		CGdiObject *oldPen = dc.SelectObject(&pens[k == 0 ? 1 : 0]); 
		dc.MoveTo(makePoint(0, (float) k));
		dc.LineTo(makePoint(1, (float) k));
		dc.SelectObject(oldPen);
	}
}

BOOL CurveEdit::OnInitDialog() 
{
	CDialog::OnInitDialog();
	
	((CButton *) GetDlgItem(IDC_ADD_POINT))->SetCheck(1);
	
	_NumSamplesDlg = new CEditableRangeUInt(std::string("FLOAT_CURVE_NB_SAMPLE"), _Node, 1, 512);
	_NumSampleWrapper.CE = this;
	_NumSamplesDlg->setWrapper(&_NumSampleWrapper);
	_NumSamplesDlg->init(80, 225, this);
	_NumSamplesDlg->enableLowerBound(1, true);
	m_SmoothingOn = Curve->hasSmoothing();

	UpdateData(FALSE);
	return TRUE;  // return TRUE unless you set the focus to a control
	              // EXCEPTION: OCX Property Pages should return FALSE
}

void CurveEdit::OnMovePoint() 
{
	_State = Move;
	_SelectedCtrlPoint = -1;
	Invalidate();
}

void CurveEdit::OnAddPoint() 
{
	_State = Create;
	_SelectedCtrlPoint = -1;
	Invalidate();	
}

void CurveEdit::OnRemovePoint() 
{
	_State = Remove;
	_SelectedCtrlPoint = -1;
	Invalidate();		
}

void CurveEdit::OnDisplayInterpolation() 
{
	Invalidate();	
}



float CurveEdit::getSampledValue(float date) const
{
	nlassert(Curve);
	nlassert(date >=0 && date < 1);
	NLMISC::OptFastFloorBegin();
	float result = (*Curve)(date);
	NLMISC::OptFastFloorEnd();
	return result;
}

void CurveEdit::OnSmoothingOn() 
{
	UpdateData();
	nlassert(Curve);
	Curve->enableSmoothing(m_SmoothingOn ? true : false /* perf; warning */);
	invalidate();
}


void CurveEdit::OnLastEqualFirst()
{
	CCtrlPoint pt = Curve->getControlPoint(0);
	pt.Date = Curve->getControlPoint(Curve->getNumCtrlPoints() - 1).Date;
	Curve->setCtrlPoint(Curve->getNumCtrlPoints() - 1, pt);
	invalidate();	
}

void CurveEdit::OnCenterCurve() 
{
	scaleMinMax();
	Invalidate();
}

void CurveEdit::OnFirstEqualLast() 
{
	CCtrlPoint pt = Curve->getControlPoint(Curve->getNumCtrlPoints() - 1);
	pt.Date = Curve->getControlPoint(0).Date;
	Curve->setCtrlPoint(0, pt);
	invalidate();	
}

void CurveEdit::OnDestroy() 
{
	CDialog::OnDestroy();	
}

void CurveEdit::init(CWnd *pParent)
{	
	CDialog::Create(IDD_CURVE_EDIT, pParent);	
	ShowWindow(SW_SHOW);
}


void CurveEdit::OnClose() 
{
	CDialog::OnClose();	
	_PN->childPopupClosed(this);
}

void CurveEdit::invalidate()
{
	if (_Node)
	{
		_Node->setModified(true);
	}
	Invalidate();
}