/*
 * EffectVis.cpp
 * -------------
 * Purpose: Implementation of parameter visualisation dialog.
 * Notes  : (currenlty none)
 * Authors: OpenMPT Devs
 * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
 */


#include "stdafx.h"
#include "Mptrack.h"
#include "Mainfrm.h"
#include "Childfrm.h"
#include "Moddoc.h"
#include "Globals.h"
#include "View_pat.h"
#include "EffectVis.h"


OPENMPT_NAMESPACE_BEGIN

CEffectVis::EditAction CEffectVis::m_nAction = CEffectVis::kAction_OverwriteFX;

IMPLEMENT_DYNAMIC(CEffectVis, CDialog)
CEffectVis::CEffectVis(CViewPattern *pViewPattern, ROWINDEX startRow, ROWINDEX endRow, CHANNELINDEX nchn, CModDoc &modDoc, PATTERNINDEX pat)
	: effectInfo(modDoc.GetSoundFile())
	, m_ModDoc(modDoc)
	, m_SndFile(modDoc.GetSoundFile())
	, m_pViewPattern(pViewPattern)
{
	m_nFillEffect = effectInfo.GetIndexFromEffect(CMD_SMOOTHMIDI, 0);
	m_templatePCNote.Set(NOTE_PCS, 1, 0, 0);
	UpdateSelection(startRow, endRow, nchn, pat);
}

BEGIN_MESSAGE_MAP(CEffectVis, CDialog)
	ON_WM_ERASEBKGND()
	ON_WM_PAINT()
	ON_WM_SIZE()
	ON_WM_LBUTTONDOWN()
	ON_WM_LBUTTONUP()
	ON_WM_MOUSEMOVE()
	ON_WM_RBUTTONDOWN()
	ON_WM_RBUTTONUP()
	ON_CBN_SELCHANGE(IDC_VISACTION,		&CEffectVis::OnActionChanged)
	ON_CBN_SELCHANGE(IDC_VISEFFECTLIST,	&CEffectVis::OnEffectChanged)
END_MESSAGE_MAP()

void CEffectVis::DoDataExchange(CDataExchange* pDX)
{
	CDialog::DoDataExchange(pDX);
	DDX_Control(pDX, IDC_VISSTATUS, m_edVisStatus);
	DDX_Control(pDX, IDC_VISEFFECTLIST, m_cmbEffectList);
	DDX_Control(pDX, IDC_VISACTION, m_cmbActionList);
}

void CEffectVis::OnActionChanged()
{
	m_nAction = static_cast<EditAction>(m_cmbActionList.GetItemData(m_cmbActionList.GetCurSel()));
	if (m_nAction == kAction_FillPC
		|| m_nAction == kAction_OverwritePC
		|| m_nAction == kAction_Preserve)
		m_cmbEffectList.EnableWindow(FALSE);
	else
		m_cmbEffectList.EnableWindow(TRUE);

}

void CEffectVis::OnEffectChanged()
{
	m_nFillEffect = static_cast<UINT>(m_cmbEffectList.GetItemData(m_cmbEffectList.GetCurSel()));
}

void CEffectVis::OnPaint()
{
	CPaintDC dc(this); // device context for painting
	ShowVis(&dc);

}

uint16 CEffectVis::GetParam(ROWINDEX row) const
{
	uint16 paramValue = 0;

	if(m_SndFile.Patterns.IsValidPat(m_nPattern))
	{
		const ModCommand &m = *m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan);
		if (m.IsPcNote())
		{
			paramValue = m.GetValueEffectCol();
		} else
		{
			paramValue = m.param;
		}
	}

	return paramValue;
}

// Sets a row's param value based on the vertical cursor position.
// Sets either plain pattern effect parameter or PC note parameter
// as appropriate, depending on contents of row.
void CEffectVis::SetParamFromY(ROWINDEX row, int y)
{
	if(!m_SndFile.Patterns.IsValidPat(m_nPattern))
		return;

	ModCommand &m = *m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan);
	if (IsPcNote(row))
	{
		uint16 param = ScreenYToPCParam(y);
		m.SetValueEffectCol(param);
	} else
	{
		ModCommand::PARAM param = ScreenYToFXParam(y);
		// Cap the parameter value as appropriate, based on effect type (e.g. Zxx gets capped to [0x00,0x7F])
		effectInfo.GetEffectFromIndex(effectInfo.GetIndexFromEffect(m.command, param), param);
		m.param = param;
	}
}


EffectCommand CEffectVis::GetCommand(ROWINDEX row) const
{
	if(m_SndFile.Patterns.IsValidPat(m_nPattern))
		return static_cast<EffectCommand>(m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan)->command);
	else
		return CMD_NONE;
}

void CEffectVis::SetCommand(ROWINDEX row, EffectCommand command)
{
	if(m_SndFile.Patterns.IsValidPat(m_nPattern))
	{
		ModCommand &m = *m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan);
		if(m.IsPcNote())
		{
			// Clear PC note
			m.note = 0;
			m.instr = 0;
			m.volcmd = VOLCMD_NONE;
			m.vol = 0;
		}
		m.command = command;
	}
}

int CEffectVis::RowToScreenX(ROWINDEX row) const
{
	if ((row >= m_startRow) || (row <= m_endRow))
		return mpt::saturate_round<int>(m_rcDraw.left + m_innerBorder + (row - m_startRow) * m_pixelsPerRow);
	return -1;
}


int CEffectVis::RowToScreenY(ROWINDEX row) const
{
	int screenY = -1;

	if(m_SndFile.Patterns.IsValidPat(m_nPattern))
	{
		const ModCommand &m = *m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan);
		if (m.IsPcNote())
		{
			uint16 paramValue = m.GetValueEffectCol();
			screenY = PCParamToScreenY(paramValue);
		} else
		{
			uint16 paramValue = m.param;
			screenY = FXParamToScreenY(paramValue);
		}
	}

	return screenY;
}

int CEffectVis::FXParamToScreenY(uint16 param) const
{
	if(param >= 0x00 && param <= 0xFF)
		return mpt::saturate_round<int>(m_rcDraw.bottom - param * m_pixelsPerFXParam);
	return -1;
}

int CEffectVis::PCParamToScreenY(uint16 param) const
{
	if(param >= 0x00 && param <= ModCommand::maxColumnValue)
		return mpt::saturate_round<int>(m_rcDraw.bottom - param*m_pixelsPerPCParam);
	return -1;
}

ModCommand::PARAM CEffectVis::ScreenYToFXParam(int y) const
{
	if(y <= FXParamToScreenY(0xFF))
		return 0xFF;

	if(y >= FXParamToScreenY(0x00))
		return 0x00;

	return mpt::saturate_round<ModCommand::PARAM>((m_rcDraw.bottom - y) / m_pixelsPerFXParam);
}

uint16 CEffectVis::ScreenYToPCParam(int y) const
{
	if(y <= PCParamToScreenY(ModCommand::maxColumnValue))
		return ModCommand::maxColumnValue;

	if(y >= PCParamToScreenY(0x00))
		return 0x00;

	return mpt::saturate_round<uint16>((m_rcDraw.bottom - y) / m_pixelsPerPCParam);
}

ROWINDEX CEffectVis::ScreenXToRow(int x) const
{
	if(x <= RowToScreenX(m_startRow))
		return m_startRow;

	if(x >= RowToScreenX(m_endRow))
		return m_endRow;

	return mpt::saturate_round<ROWINDEX>(m_startRow + (x - m_innerBorder) / m_pixelsPerRow);
}


void CEffectVis::DrawGrid()
{
	// Lots of room for optimisation here.
	// Draw vertical grid lines
	ROWINDEX nBeat = m_SndFile.m_nDefaultRowsPerBeat, nMeasure = m_SndFile.m_nDefaultRowsPerMeasure;
	if(m_SndFile.Patterns[m_nPattern].GetOverrideSignature())
	{
		nBeat = m_SndFile.Patterns[m_nPattern].GetRowsPerBeat();
		nMeasure = m_SndFile.Patterns[m_nPattern].GetRowsPerMeasure();
	}

	m_dcGrid.FillSolidRect(&m_rcDraw, 0);
	auto oldPen = m_dcGrid.SelectStockObject(DC_PEN);
	for(ROWINDEX row = m_startRow; row <= m_endRow; row++)
	{
		if(row % nMeasure == 0)
			m_dcGrid.SetDCPenColor(RGB(0xFF, 0xFF, 0xFF));
		else if(row % nBeat == 0)
			m_dcGrid.SetDCPenColor(RGB(0x99, 0x99, 0x99));
		else
			m_dcGrid.SetDCPenColor(RGB(0x55, 0x55, 0x55));
		int x1 = RowToScreenX(row);
		m_dcGrid.MoveTo(x1, m_rcDraw.top);
		m_dcGrid.LineTo(x1, m_rcDraw.bottom);
	}

	// Draw horizontal grid lines
	constexpr UINT numHorizontalLines = 4;
	for(UINT i = 0; i < numHorizontalLines; i++)
	{
		COLORREF c = 0;
		switch(i % 4)
		{
		case 0: c = RGB(0x00, 0x00, 0x00); break;
		case 1: c = RGB(0x40, 0x40, 0x40); break;
		case 2: c = RGB(0x80, 0x80, 0x80); break;
		case 3: c = RGB(0xCC, 0xCC, 0xCC); break;
		}
		m_dcGrid.SetDCPenColor(c);
		int y1 = m_rcDraw.bottom / numHorizontalLines * i;
		m_dcGrid.MoveTo(m_rcDraw.left + m_innerBorder, y1);
		m_dcGrid.LineTo(m_rcDraw.right - m_innerBorder, y1);
	}
	m_dcGrid.SelectObject(oldPen);
}


void CEffectVis::SetPlayCursor(PATTERNINDEX nPat, ROWINDEX nRow)
{
	if(nPat == m_nPattern && nRow == m_nOldPlayPos)
		return;

	if(m_nOldPlayPos >= m_startRow && m_nOldPlayPos <= m_endRow)
	{
		// erase current playpos
		int x1 = RowToScreenX(m_nOldPlayPos);
		m_dcPlayPos.SelectStockObject(BLACK_PEN);
		m_dcPlayPos.MoveTo(x1,m_rcDraw.top);
		m_dcPlayPos.LineTo(x1,m_rcDraw.bottom);
	}

	if((nRow < m_startRow) || (nRow > m_endRow) || (nPat != m_nPattern))
		return;

	int x1 = RowToScreenX(nRow);
	m_dcPlayPos.SelectStockObject(DC_PEN);
	m_dcPlayPos.SetDCPenColor(TrackerSettings::Instance().rgbCustomColors[MODCOLOR_SAMPLE]);
	m_dcPlayPos.MoveTo(x1,m_rcDraw.top);
	m_dcPlayPos.LineTo(x1,m_rcDraw.bottom);

	m_nOldPlayPos = nRow;
	InvalidateRect(NULL, FALSE);
}


void CEffectVis::ShowVis(CDC *pDC)
{
	if (m_forceRedraw)
	{
		m_forceRedraw = false;

		// if we already have a memory dc, destroy it (this occurs for a re-size)
		if (m_dcGrid.m_hDC)
		{
			m_dcGrid.SelectObject(m_pbOldGrid);
			m_dcGrid.DeleteDC();

			m_dcNodes.SelectObject(m_pbOldNodes);
			m_dcNodes.DeleteDC();

			m_dcPlayPos.SelectObject(m_pbOldPlayPos);
			m_dcPlayPos.DeleteDC();

			m_bPlayPos.DeleteObject();
			m_bGrid.DeleteObject();
			m_bNodes.DeleteObject();
		}

		// create a memory based dc for drawing the grid
		m_dcGrid.CreateCompatibleDC(pDC);
		m_bGrid.CreateCompatibleBitmap(pDC, m_rcDraw.Width(), m_rcDraw.Height());
		m_pbOldGrid = *m_dcGrid.SelectObject(&m_bGrid);

		// create a memory based dc for drawing the nodes
		m_dcNodes.CreateCompatibleDC(pDC);
		m_bNodes.CreateCompatibleBitmap(pDC, m_rcDraw.Width(), m_rcDraw.Height());
		m_pbOldNodes = *m_dcNodes.SelectObject(&m_bNodes);

		// create a memory based dc for drawing the nodes
		m_dcPlayPos.CreateCompatibleDC(pDC);
		m_bPlayPos.CreateCompatibleBitmap(pDC, m_rcDraw.Width(), m_rcDraw.Height());
		m_pbOldPlayPos = *m_dcPlayPos.SelectObject(&m_bPlayPos);

		SetPlayCursor(m_nPattern, m_nOldPlayPos);
		DrawGrid();
		DrawNodes();
	}

	// display the new image, combining the nodes with the grid
	ShowVisImage(pDC);

}


void CEffectVis::ShowVisImage(CDC *pDC)
{
	// to avoid flicker, establish a memory dc, draw to it
	// and then BitBlt it to the destination "pDC"
	CDC memDC;
	memDC.CreateCompatibleDC(pDC);
	if (!memDC)
		return;

	CBitmap memBitmap;
	memBitmap.CreateCompatibleBitmap(pDC, m_rcDraw.Width(), m_rcDraw.Height());
	CBitmap *oldBitmap = memDC.SelectObject(&memBitmap);

	// make sure we have the bitmaps
	if (!m_dcGrid.m_hDC)
		return;
	if (!m_dcNodes.m_hDC)
		return;
	if (!m_dcPlayPos.m_hDC)
		return;

	if (memDC.m_hDC != nullptr)
	{
		// draw the grid
		memDC.BitBlt(0, 0, m_rcDraw.Width(), m_rcDraw.Height(), &m_dcGrid, 0, 0, SRCCOPY);

		// merge the nodes image with the grid
		memDC.TransparentBlt(0, 0, m_rcDraw.Width(), m_rcDraw.Height(), &m_dcNodes, 0, 0, m_rcDraw.Width(), m_rcDraw.Height(), 0x00000000);
		// further merge the playpos
		memDC.TransparentBlt(0, 0, m_rcDraw.Width(), m_rcDraw.Height(), &m_dcPlayPos, 0, 0, m_rcDraw.Width(), m_rcDraw.Height(), 0x00000000);

		// copy the resulting bitmap to the destination
		pDC->BitBlt(0, 0, m_rcDraw.Width(), m_rcDraw.Height(), &memDC, 0, 0, SRCCOPY);
	}

	memDC.SelectObject(oldBitmap);

}


void CEffectVis::DrawNodes()
{
	if(m_rcDraw.IsRectEmpty())
		return;

	//Draw
	const int lineWidth = Util::ScalePixels(1, m_hWnd);
	const int nodeSizeHalf = m_nodeSizeHalf;
	const int nodeSizeHalf2 = nodeSizeHalf - lineWidth + 1;
	const int nodeSize = 2 * nodeSizeHalf + 1;

	//erase
	if ((ROWINDEX)m_nRowToErase < m_startRow || m_nParamToErase < 0)
	{
		m_dcNodes.FillSolidRect(&m_rcDraw, 0);
	} else
	{
		int x = RowToScreenX(m_nRowToErase);
		CRect r(x - nodeSizeHalf, m_rcDraw.top, x + nodeSizeHalf + 1, m_rcDraw.bottom);
		m_dcNodes.FillSolidRect(&r, 0);
	}

	for (ROWINDEX row = m_startRow; row <= m_endRow; row++)
	{
		COLORREF col = IsPcNote(row) ? RGB(0xFF, 0xFF, 0x00) : RGB(0xD0, 0xFF, 0xFF);
		int x = RowToScreenX(row);
		int y = RowToScreenY(row);
		m_dcNodes.FillSolidRect(x - nodeSizeHalf, y - nodeSizeHalf, nodeSize, lineWidth, col);  // Top
		m_dcNodes.FillSolidRect(x + nodeSizeHalf2, y - nodeSizeHalf, lineWidth, nodeSize, col); // Right
		m_dcNodes.FillSolidRect(x - nodeSizeHalf, y + nodeSizeHalf2, nodeSize, lineWidth, col); // Bottom
		m_dcNodes.FillSolidRect(x - nodeSizeHalf, y - nodeSizeHalf, lineWidth, nodeSize, col);  // Left
	}

}

void CEffectVis::InvalidateRow(int row)
{
	if (((UINT)row < m_startRow) ||  ((UINT)row > m_endRow)) return;

//It seems this optimisation doesn't work properly yet.	Disable in Update()

	int x = RowToScreenX(row);
	invalidated.bottom = m_rcDraw.bottom;
	invalidated.top = m_rcDraw.top;
	invalidated.left = x - m_nodeSizeHalf;
	invalidated.right = x + m_nodeSizeHalf + 1;
	InvalidateRect(&invalidated, FALSE);
}


void CEffectVis::OpenEditor(CWnd *parent)
{
	Create(IDD_EFFECTVISUALIZER, parent);
	m_forceRedraw = true;

	if(TrackerSettings::Instance().effectVisWidth > 0 && TrackerSettings::Instance().effectVisHeight > 0)
	{
		WINDOWPLACEMENT wnd;
		wnd.length = sizeof(wnd);
		GetWindowPlacement(&wnd);
		wnd.showCmd = SW_SHOWNOACTIVATE;
		CRect rect = wnd.rcNormalPosition;
		if(TrackerSettings::Instance().effectVisX > int32_min && TrackerSettings::Instance().effectVisY > int32_min)
		{
			CRect mainRect;
			CMainFrame::GetMainFrame()->GetWindowRect(mainRect);
			rect.left = mainRect.left + MulDiv(TrackerSettings::Instance().effectVisX, Util::GetDPIx(m_hWnd), 96);
			rect.top = mainRect.top + MulDiv(TrackerSettings::Instance().effectVisY, Util::GetDPIx(m_hWnd), 96);
		}
		rect.right = rect.left + MulDiv(TrackerSettings::Instance().effectVisWidth, Util::GetDPIx(m_hWnd), 96);
		rect.bottom = rect.top + MulDiv(TrackerSettings::Instance().effectVisHeight, Util::GetDPIx(m_hWnd), 96);
		wnd.rcNormalPosition = rect;
		SetWindowPlacement(&wnd);
	}

	ShowWindow(SW_SHOW);
}


void CEffectVis::OnClose()
{
	DoClose();
}


void CEffectVis::OnOK()
{
	OnClose();
}


void CEffectVis::OnCancel()
{
	OnClose();
}


void CEffectVis::DoClose()
{
	WINDOWPLACEMENT wnd;
	wnd.length = sizeof(wnd);
	GetWindowPlacement(&wnd);
	CRect mainRect;
	CMainFrame::GetMainFrame()->GetWindowRect(mainRect);

	CRect rect = wnd.rcNormalPosition;
	rect.MoveToXY(rect.left - mainRect.left, rect.top - mainRect.top);
	TrackerSettings::Instance().effectVisWidth = MulDiv(rect.Width(), 96, Util::GetDPIx(m_hWnd));
	TrackerSettings::Instance().effectVisHeight = MulDiv(rect.Height(), 96, Util::GetDPIy(m_hWnd));
	TrackerSettings::Instance().effectVisX = MulDiv(rect.left, 96, Util::GetDPIx(m_hWnd));
	TrackerSettings::Instance().effectVisY = MulDiv(rect.top, 96, Util::GetDPIy(m_hWnd));

	m_dcGrid.SelectObject(m_pbOldGrid);
	m_dcGrid.DeleteDC();
	m_dcNodes.SelectObject(m_pbOldNodes);
	m_dcNodes.DeleteDC();
	m_dcPlayPos.SelectObject(m_pbOldPlayPos);
	m_dcPlayPos.DeleteDC();

	m_bGrid.DeleteObject();
	m_bNodes.DeleteObject();
	m_bPlayPos.DeleteObject();

	DestroyWindow();
}


void CEffectVis::PostNcDestroy()
{
	m_pViewPattern->m_pEffectVis = nullptr;
}


void CEffectVis::OnSize(UINT nType, int cx, int cy)
{
	MPT_UNREFERENCED_PARAMETER(nType);
	MPT_UNREFERENCED_PARAMETER(cx);
	MPT_UNREFERENCED_PARAMETER(cy);
	GetClientRect(&m_rcFullWin);
	m_rcDraw.SetRect(m_rcFullWin.left, m_rcFullWin.top, m_rcFullWin.right, m_rcFullWin.bottom - m_marginBottom);

	const int actionListWidth = Util::ScalePixels(170, m_hWnd);
	const int commandListWidth = Util::ScalePixels(160, m_hWnd);

	if (IsWindow(m_edVisStatus.m_hWnd))
		m_edVisStatus.SetWindowPos(this, m_rcFullWin.left, m_rcDraw.bottom, m_rcFullWin.right-commandListWidth-actionListWidth, m_rcFullWin.bottom-m_rcDraw.bottom, SWP_NOACTIVATE|SWP_NOCOPYBITS|SWP_SHOWWINDOW|SWP_NOZORDER);
	if (IsWindow(m_cmbActionList))
		m_cmbActionList.SetWindowPos(this,  m_rcFullWin.right-commandListWidth-actionListWidth, m_rcDraw.bottom, actionListWidth, m_rcFullWin.bottom-m_rcDraw.bottom, SWP_NOACTIVATE|SWP_NOCOPYBITS|SWP_SHOWWINDOW|SWP_NOZORDER);
	if (IsWindow(m_cmbEffectList))
		m_cmbEffectList.SetWindowPos(this,  m_rcFullWin.right-commandListWidth, m_rcDraw.bottom, commandListWidth, m_rcFullWin.bottom-m_rcDraw.bottom, SWP_NOACTIVATE|SWP_NOCOPYBITS|SWP_SHOWWINDOW|SWP_NOZORDER);

	if(m_nRows)
		m_pixelsPerRow = (float)(m_rcDraw.Width() - m_innerBorder * 2) / (float)m_nRows;
	else
		m_pixelsPerRow = 1;
	m_pixelsPerFXParam = (float)(m_rcDraw.Height())/(float)0xFF;
	m_pixelsPerPCParam = (float)(m_rcDraw.Height())/(float)ModCommand::maxColumnValue;
	m_forceRedraw = true;
	InvalidateRect(NULL, FALSE);	//redraw everything
}

void CEffectVis::Update()
{
	DrawNodes();
	if (::IsWindow(m_hWnd))
	{
		OnPaint();
		if (m_nRowToErase<0)
			InvalidateRect(NULL, FALSE);	// redraw everything
		else
		{
			InvalidateRow(m_nRowToErase);
			m_nParamToErase=-1;
			m_nRowToErase=-1;
		}

	}
}

void CEffectVis::UpdateSelection(ROWINDEX startRow, ROWINDEX endRow, CHANNELINDEX nchn, PATTERNINDEX pat)
{
	m_startRow = startRow;
	m_endRow = endRow;
	m_nRows = endRow - startRow;
	m_nChan = nchn;
	m_nPattern = pat;

	//Check pattern, start row and channel exist
	if(!m_SndFile.Patterns.IsValidPat(m_nPattern) || !m_SndFile.Patterns[m_nPattern].IsValidRow(m_startRow) || m_nChan >= m_SndFile.GetNumChannels())
	{
		DoClose();
		return;
	}

	//Check end exists
	if(!m_SndFile.Patterns[m_nPattern].IsValidRow(m_endRow))
	{
		m_endRow = m_SndFile.Patterns[m_nPattern].GetNumRows() - 1;
	}

	if(m_nRows)
		m_pixelsPerRow = (float)(m_rcDraw.Width() - m_innerBorder * 2) / (float)m_nRows;
	else
		m_pixelsPerRow = 1;
	m_pixelsPerFXParam = (float)(m_rcDraw.Height())/(float)0xFF;
	m_pixelsPerPCParam = (float)(m_rcDraw.Height())/(float)ModCommand::maxColumnValue;

	m_forceRedraw = true;
	Update();

}

void CEffectVis::OnRButtonDown(UINT nFlags, CPoint point)
{
	if (!(m_dwStatus & FXVSTATUS_LDRAGGING))
	{
		SetFocus();
		SetCapture();

		m_nDragItem = ScreenXToRow(point.x);
		m_dwStatus |= FXVSTATUS_RDRAGGING;
		m_ModDoc.GetPatternUndo().PrepareUndo(static_cast<PATTERNINDEX>(m_nPattern), m_nChan, m_nDragItem, 1, 1, "Parameter Editor entry");
		OnMouseMove(nFlags, point);
	}

	CDialog::OnRButtonDown(nFlags, point);
}

void CEffectVis::OnRButtonUp(UINT nFlags, CPoint point)
{
	ReleaseCapture();
	m_dwStatus = 0x00;
	m_nDragItem = -1;
	CDialog::OnRButtonUp(nFlags, point);
}

void CEffectVis::OnMouseMove(UINT nFlags, CPoint point)
{
	CDialog::OnMouseMove(nFlags, point);

	ROWINDEX row = ScreenXToRow(point.x);

	if ((m_dwStatus & FXVSTATUS_RDRAGGING) && (m_nDragItem>=0) )
	{
		m_nRowToErase = m_nDragItem;
		m_nParamToErase = GetParam(m_nDragItem);

		MakeChange(m_nDragItem, point.y);
	} else if ((m_dwStatus & FXVSTATUS_LDRAGGING))
	{
		// Interpolate if we detect that rows have been skipped but the left mouse button was not released.
		// This ensures we produce a smooth curve even when we are not notified of mouse movements at a high frequency (e.g. if CPU usage is high)
		const int steps = std::abs((int)row - (int)m_nLastDrawnRow);
		if (m_nLastDrawnRow != ROWINDEX_INVALID && m_nLastDrawnRow > m_startRow && steps > 1)
		{
			int direction = ((int)(row - m_nLastDrawnRow) > 0) ? 1 : -1;
			float factor = (float)(point.y - m_nLastDrawnY)/(float)steps + 0.5f;

			int currentRow;
			for (int i=1; i<=steps; i++)
			{
				currentRow = m_nLastDrawnRow+(direction*i);
				int interpolatedY = mpt::saturate_round<int>(m_nLastDrawnY + ((float)i * factor));
				MakeChange(currentRow, interpolatedY);
			}

			//Don't use single value update
			m_nRowToErase = -1;
			m_nParamToErase = -1;
		} else
		{
			m_nRowToErase = -1;
			m_nParamToErase = -1;
			MakeChange(row, point.y);
		}

		// Remember last modified point in case we need to interpolate
		m_nLastDrawnRow = row;
		m_nLastDrawnY = point.y;
	}
	//update status bar
	CString status;
	CString effectName;
	uint16 paramValue;


	if (IsPcNote(row))
	{
		paramValue = ScreenYToPCParam(point.y);
		effectName.Format(_T("%s"), _T("Param Control")); // TODO - show smooth & plug+param
	} else
	{
		paramValue = ScreenYToFXParam(point.y);
		effectInfo.GetEffectInfo(effectInfo.GetIndexFromEffect(GetCommand(row), ModCommand::PARAM(GetParam(row))), &effectName, true);
	}

	status.Format(_T("Pat: %d\tChn: %d\tRow: %d\tVal: %02X (%03d) [%s]"),
				m_nPattern, m_nChan+1, static_cast<signed int>(row), paramValue, paramValue, effectName.GetString());
	m_edVisStatus.SetWindowText(status);
}

void CEffectVis::OnLButtonDown(UINT nFlags, CPoint point)
{
	if (!(m_dwStatus & FXVSTATUS_RDRAGGING))
	{
		SetFocus();
		SetCapture();

		m_nDragItem = ScreenXToRow(point.x);
		m_dwStatus |= FXVSTATUS_LDRAGGING;
		m_ModDoc.GetPatternUndo().PrepareUndo(static_cast<PATTERNINDEX>(m_nPattern), m_nChan, m_startRow, 1, m_endRow - m_startRow + 1, "Parameter Editor entry");
		OnMouseMove(nFlags, point);
	}

	CDialog::OnLButtonDown(nFlags, point);
}

void CEffectVis::OnLButtonUp(UINT nFlags, CPoint point)
{
	ReleaseCapture();
	m_dwStatus = 0x00;
	CDialog::OnLButtonUp(nFlags, point);
	m_nLastDrawnRow = ROWINDEX_INVALID;
}


BOOL CEffectVis::OnInitDialog()
{
	CDialog::OnInitDialog();

	int dpi = Util::GetDPIx(m_hWnd);
	m_nodeSizeHalf = MulDiv(3, dpi, 96);
	m_marginBottom = MulDiv(20, dpi, 96);
	m_innerBorder = MulDiv(4, dpi, 96);

	// If first selected row is a PC event (or some other row but there aren't any other effects), default to PC note overwrite mode
	// and use it as a template for new PC notes that will be created via the visualiser.
	bool isPCevent = IsPcNote(m_startRow);
	if(!isPCevent)
	{
		for(ROWINDEX row = m_startRow; row <= m_endRow; row++)
		{
			if(IsPcNote(row))
			{
				isPCevent = true;
			} else if(GetCommand(row) != CMD_NONE)
			{
				isPCevent = false;
				break;
			}
		}
	}

	if(m_ModDoc.GetModType() == MOD_TYPE_MPT && isPCevent)
	{
		m_nAction = kAction_OverwritePC;
		if(m_SndFile.Patterns.IsValidPat(m_nPattern))
		{
			ModCommand &m = *m_SndFile.Patterns[m_nPattern].GetpModCommand(m_startRow, m_nChan);
			m_templatePCNote.Set(m.note, m.instr, m.GetValueVolCol(), 0);
		}
		m_cmbEffectList.EnableWindow(FALSE);
	} else
	{
		// Otherwise, default to FX overwrite and
		// use effect of first selected row as default effect type
		m_nAction = kAction_OverwriteFX;
		m_nFillEffect = effectInfo.GetIndexFromEffect(GetCommand(m_startRow), ModCommand::PARAM(GetParam(m_startRow)));
		if (m_nFillEffect < 0 || m_nFillEffect >= MAX_EFFECTS)
			m_nFillEffect = effectInfo.GetIndexFromEffect(CMD_SMOOTHMIDI, 0);
	}


	CString s;
	UINT numfx = effectInfo.GetNumEffects();
	m_cmbEffectList.ResetContent();
	int k;
	for (UINT i=0; i<numfx; i++)
	{
		if (effectInfo.GetEffectInfo(i, &s, true))
		{
			k =m_cmbEffectList.AddString(s);
			m_cmbEffectList.SetItemData(k, i);
			if ((int)i == m_nFillEffect)
				m_cmbEffectList.SetCurSel(k);
		}
	}

	m_cmbActionList.ResetContent();
	m_cmbActionList.SetItemData(m_cmbActionList.AddString(_T("Overwrite with effect:")), kAction_OverwriteFX);
	m_cmbActionList.SetItemData(m_cmbActionList.AddString(_T("Overwrite effect next to note:")), kAction_OverwriteFXWithNote);
	m_cmbActionList.SetItemData(m_cmbActionList.AddString(_T("Fill blanks with effect:")), kAction_FillFX);
	if (m_ModDoc.GetModType() == MOD_TYPE_MPT)
	{
		m_cmbActionList.SetItemData(m_cmbActionList.AddString(_T("Overwrite with PC note")), kAction_OverwritePC);
		m_cmbActionList.SetItemData(m_cmbActionList.AddString(_T("Fill blanks with PC note")), kAction_FillPC);
	}
	m_cmbActionList.SetItemData(m_cmbActionList.AddString(_T("Never change effect type")), kAction_Preserve);

	m_cmbActionList.SetCurSel(m_nAction);
	return true;
}

void CEffectVis::MakeChange(ROWINDEX row, int y)
{
	if(!m_SndFile.Patterns.IsValidPat(m_nPattern))
		return;

	ModCommand &m = *m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan);

	switch (m_nAction)
	{
		case kAction_FillFX:
			// Only set command if there isn't a command already at this row and it's not a PC note
			if (GetCommand(row) == CMD_NONE && !IsPcNote(row))
			{
				SetCommand(row, effectInfo.GetEffectFromIndex(m_nFillEffect));
			}
			// Always set param
			SetParamFromY(row, y);
			break;

		case kAction_OverwriteFXWithNote:
			if(!m_SndFile.Patterns.IsValidPat(m_nPattern))
				break;
			if(!m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan)->IsNote())
				break;
			[[fallthrough]];
		case kAction_OverwriteFX:
			// Always set command and param. Blows away any PC notes.
			SetCommand(row, effectInfo.GetEffectFromIndex(m_nFillEffect));
			SetParamFromY(row, y);
			break;

		case kAction_FillPC:
			// Fill only empty slots with PC notes - leave other slots alone.
			if (m.IsEmpty())
			{
				SetPcNote(row);
			}
			// Always set param
			SetParamFromY(row, y);
			break;

		case kAction_OverwritePC:
			// Always convert to PC Note and set param value
			SetPcNote(row);
			SetParamFromY(row, y);
			break;

		case kAction_Preserve:
			if (GetCommand(row) != CMD_NONE || IsPcNote(row))
			{
				// Only set param if we have an effect type or if this is a PC note.
				// Never change the effect type.
				SetParamFromY(row, y);
			}
			break;

	}

	m_ModDoc.SetModified();
	m_ModDoc.UpdateAllViews(nullptr, PatternHint(m_nPattern).Data());
}

void CEffectVis::SetPcNote(ROWINDEX row)
{
	if(!m_SndFile.Patterns.IsValidPat(m_nPattern))
		return;

	ModCommand &m = *m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan);
	m.Set(m_templatePCNote.note, m_templatePCNote.instr, m_templatePCNote.GetValueVolCol(), 0);
}

bool CEffectVis::IsPcNote(ROWINDEX row) const
{
	if(m_SndFile.Patterns.IsValidPat(m_nPattern))
		return m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan)->IsPcNote();
	else
		return false;
}


OPENMPT_NAMESPACE_END