/*
 * TempoSwingDialog.cpp
 * --------------------
 * Purpose: Implementation of the tempo swing configuration dialog.
 * Notes  : (currently none)
 * Authors: OpenMPT Devs
 * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
 */


#include "stdafx.h"
#include "TempoSwingDialog.h"
#include "Mainfrm.h"


OPENMPT_NAMESPACE_BEGIN

void CTempoSwingDlg::RowCtls::SetValue(TempoSwing::value_type v)
{
	int32 val = Util::muldivr(static_cast<int32>(v) - TempoSwing::Unity, CTempoSwingDlg::SliderUnity, TempoSwing::Unity);
	valueSlider.SetPos(val);
}


TempoSwing::value_type CTempoSwingDlg::RowCtls::GetValue() const
{
	return Util::muldivr(valueSlider.GetPos(), TempoSwing::Unity, SliderUnity) + TempoSwing::Unity;
}


BEGIN_MESSAGE_MAP(CTempoSwingDlg, CDialog)
	//{{AFX_MSG_MAP(CTempoSwingDlg)
	ON_WM_VSCROLL()
	ON_COMMAND(IDC_BUTTON1,	&CTempoSwingDlg::OnReset)
	ON_COMMAND(IDC_BUTTON2,	&CTempoSwingDlg::OnUseGlobal)
	ON_COMMAND(IDC_CHECK1,	&CTempoSwingDlg::OnToggleGroup)
	ON_EN_CHANGE(IDC_EDIT1, &CTempoSwingDlg::OnGroupChanged)
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()

int CTempoSwingDlg::m_groupSize = 1;

CTempoSwingDlg::CTempoSwingDlg(CWnd *parent, const TempoSwing &currentTempoSwing, CSoundFile &sndFile, PATTERNINDEX pattern)
	: CDialog(IDD_TEMPO_SWING, parent)
	, m_container(*this)
	, m_scrollPos(0)
	, m_tempoSwing(currentTempoSwing)
	, m_origTempoSwing(pattern == PATTERNINDEX_INVALID ? sndFile.m_tempoSwing : sndFile.Patterns[pattern].GetTempoSwing())
	, m_sndFile(sndFile)
	, m_pattern(pattern)
{
	m_groupSize = std::min(m_groupSize, static_cast<int>(m_tempoSwing.size()));
}


void CTempoSwingDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialog::DoDataExchange(pDX);
	DDX_Control(pDX, IDC_CHECK1, m_checkGroup);
	DDX_Control(pDX, IDC_SCROLLBAR1, m_scrollBar);
	DDX_Control(pDX, IDC_CONTAINER, m_container);
}


BOOL CTempoSwingDlg::OnInitDialog()
{
	struct Measurements
	{
		enum
		{
			edRowLabelWidth = 64,	// Label "Row 999:"
			edSliderWidth = 220,	// Setting slider
			edSliderHeight = 20,	// Setting slider
			edValueLabelWidth = 64,	// Label "100%"
			edPaddingX = 8,			// Spacing between elements
			edPaddingY = 4,			// Spacing between elements
			edPaddingTop = 64,		// Spacing from top of dialog
			edRowHeight = edSliderHeight + edPaddingY, // Height of one set of controls
			edFooterHeight = 32,	// Buttons
			edScrollbarWidth = 16,	// Width of optional scrollbar
		};

		const int rowLabelWidth;
		const int sliderWidth;
		const int sliderHeight;
		const int valueLabelWidth;
		const int paddingX;
		const int paddingY;
		const int paddingTop;
		const int rowHeight;
		const int footerHeight;
		const int scrollbarWidth;

		Measurements(HWND hWnd)
			: rowLabelWidth(Util::ScalePixels(edRowLabelWidth, hWnd))
			, sliderWidth(Util::ScalePixels(edSliderWidth, hWnd))
			, sliderHeight(Util::ScalePixels(edSliderHeight, hWnd))
			, valueLabelWidth(Util::ScalePixels(edValueLabelWidth, hWnd))
			, paddingX(Util::ScalePixels(edPaddingX, hWnd))
			, paddingY(Util::ScalePixels(edPaddingY, hWnd))
			, paddingTop(Util::ScalePixels(edPaddingTop, hWnd))
			, rowHeight(Util::ScalePixels(edRowHeight, hWnd))
			, footerHeight(Util::ScalePixels(edFooterHeight, hWnd))
			, scrollbarWidth(Util::ScalePixels(edScrollbarWidth, hWnd))
		{ }
	};

	CDialog::OnInitDialog();
	Measurements m(m_hWnd);
	CRect windowRect, rect;
	GetWindowRect(windowRect);
	GetClientRect(rect);
	windowRect.bottom = windowRect.top + windowRect.Height() - rect.Height();

	CRect mainWindowRect;
	CMainFrame::GetMainFrame()->GetClientRect(mainWindowRect);

	const int realHeight = static_cast<int>(m_tempoSwing.size()) * m.rowHeight;
	const int displayHeight = std::min(realHeight, static_cast<int>(mainWindowRect.bottom - windowRect.Height() - m.paddingTop - m.footerHeight));

	CRect containerRect;
	m_container.GetClientRect(containerRect);
	containerRect.bottom = displayHeight;
	m_container.SetWindowPos(nullptr, 0, m.paddingTop, rect.right - m.scrollbarWidth, containerRect.bottom, SWP_NOZORDER);
	m_container.ModifyStyleEx(0, WS_EX_CONTROLPARENT, 0);

	// Need scrollbar?
	if(realHeight > displayHeight)
	{
		SCROLLINFO info;
		info.cbSize = sizeof(info);
		info.fMask = SIF_ALL;
		info.nMin = 0;
		info.nMax = realHeight;
		info.nPage = displayHeight;
		info.nTrackPos = info.nPos = 0;
		m_scrollBar.SetScrollInfo(&info, FALSE);

		CRect scrollRect;
		m_scrollBar.GetClientRect(scrollRect);
		m_scrollBar.SetWindowPos(nullptr, containerRect.right, m.paddingTop, scrollRect.Width(), displayHeight, SWP_NOZORDER);
	} else
	{
		m_scrollBar.ShowWindow(SW_HIDE);
	}

	rect.DeflateRect(m.paddingX, 0/* m.paddingTop*/, m.paddingX + m.scrollbarWidth, 0);

	GetDlgItem(IDC_BUTTON2)->ShowWindow((m_pattern != PATTERNINDEX_INVALID) ? SW_SHOW : SW_HIDE);

	m_controls.resize(m_tempoSwing.size());
	for(size_t i = 0; i < m_controls.size(); i++)
	{
		m_controls[i] = std::make_unique<RowCtls>();
		auto &r = m_controls[i];
		// Row label
		r->rowLabel.Create(MPT_CFORMAT("Row {}:")(i + 1), WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(rect.left, rect.top, rect.right, rect.top + m.rowHeight), &m_container);
		r->rowLabel.SetFont(GetFont());

		// Value label
		r->valueLabel.Create(_T("100%"), WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(rect.right - m.valueLabelWidth, rect.top, rect.right, rect.top + m.sliderHeight), &m_container);
		r->valueLabel.SetFont(GetFont());

		// Value slider
		r->valueSlider.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP | TBS_TOOLTIPS | TBS_AUTOTICKS, CRect(rect.left + m.rowLabelWidth, rect.top, rect.right - m.valueLabelWidth, rect.top + m.sliderHeight), &m_container, 0xFFFF);
		r->valueSlider.SetFont(GetFont());
		r->valueSlider.SetRange(-SliderResolution / 2, SliderResolution / 2);
		r->valueSlider.SetTicFreq(SliderResolution / 8);
		r->valueSlider.SetPageSize(SliderResolution / 8);
		r->valueSlider.SetPos(1);	// Work around https://bugs.winehq.org/show_bug.cgi?id=41909
		r->SetValue(m_tempoSwing[i]);
		rect.MoveToY(rect.top + m.rowHeight);
	}

	((CSpinButtonCtrl *)GetDlgItem(IDC_SPIN1))->SetRange32(1, static_cast<int>(m_tempoSwing.size()));
	SetDlgItemInt(IDC_EDIT1, m_groupSize);
	OnToggleGroup();

	m_container.OnHScroll(0, 0, reinterpret_cast<CScrollBar *>(&(m_controls[0]->valueSlider)));
	rect.MoveToY(m.paddingTop + containerRect.bottom + m.paddingY);
	{
		// Buttons at dialog bottom
		CRect buttonRect;
		for(auto i : { IDOK, IDCANCEL, IDC_BUTTON2 })
		{
			auto wnd = GetDlgItem(i);
			wnd->GetWindowRect(buttonRect);
			wnd->SetWindowPos(nullptr, buttonRect.left - windowRect.left - GetSystemMetrics(SM_CXEDGE), rect.top, 0, 0, SWP_NOSIZE | SWP_NOOWNERZORDER);
		}
	}

	windowRect.bottom += displayHeight + m.paddingTop + m.footerHeight;
	SetWindowPos(nullptr, 0, 0, windowRect.Width(), windowRect.Height(), SWP_NOMOVE | SWP_NOOWNERZORDER);
	EnableToolTips();

	return TRUE;
}


void CTempoSwingDlg::OnOK()
{
	CDialog::OnOK();
	// If this is the default setup, just clear the vector.
	if(m_pattern == PATTERNINDEX_INVALID)
	{
		if(static_cast<size_t>(std::count(m_tempoSwing.begin(), m_tempoSwing.end(), static_cast<TempoSwing::value_type>(TempoSwing::Unity))) == m_tempoSwing.size())
		{
			m_tempoSwing.clear();
		}
	} else
	{
		if(m_tempoSwing == m_sndFile.m_tempoSwing)
		{
			m_tempoSwing.clear();
		}
	}
	OnClose();
}


void CTempoSwingDlg::OnCancel()
{
	CDialog::OnCancel();
	OnClose();
}


void CTempoSwingDlg::OnClose()
{
	// Restore original swing properties after preview
	if(m_pattern == PATTERNINDEX_INVALID)
	{
		m_sndFile.m_tempoSwing = m_origTempoSwing;
	} else
	{
		m_sndFile.Patterns[m_pattern].SetTempoSwing(m_origTempoSwing);
	}
}


void CTempoSwingDlg::OnReset()
{
	for(size_t i = 0; i < m_controls.size(); i++)
	{
		m_controls[i]->valueSlider.SetPos(0);
	}
	m_container.OnHScroll(0, 0, reinterpret_cast<CScrollBar *>(&(m_controls[0]->valueSlider)));
}


void CTempoSwingDlg::OnUseGlobal()
{
	if(m_sndFile.m_tempoSwing.empty())
	{
		OnReset();
		return;
	}
	for(size_t i = 0; i < m_tempoSwing.size(); i++)
	{
		m_controls[i]->SetValue(m_sndFile.m_tempoSwing[i % m_sndFile.m_tempoSwing.size()]);
	}
	m_container.OnHScroll(0, 0, reinterpret_cast<CScrollBar *>(&(m_controls[0]->valueSlider)));
}


void CTempoSwingDlg::OnToggleGroup()
{
	const BOOL checked = m_checkGroup.GetCheck() != BST_UNCHECKED;
	GetDlgItem(IDC_EDIT1)->EnableWindow(checked);
	GetDlgItem(IDC_SPIN1)->EnableWindow(checked);
}


void CTempoSwingDlg::OnGroupChanged()
{
	int val = GetDlgItemInt(IDC_EDIT1);
	if(val > 0) m_groupSize = std::min(val, static_cast<int>(m_tempoSwing.size()));
}


void CTempoSwingDlg::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar *pScrollBar)
{
	if(pScrollBar == &m_scrollBar)
	{
		// Get the minimum and maximum scrollbar positions.
		int minpos;
		int maxpos;
		pScrollBar->GetScrollRange(&minpos, &maxpos);

		SCROLLINFO sbInfo;
		pScrollBar->GetScrollInfo(&sbInfo);

		// Get the current position of scroll box.
		int curpos = pScrollBar->GetScrollPos();

		// Determine the new position of scroll box.
		switch(nSBCode)
		{
		case SB_LEFT:			// Scroll to far left.
			curpos = minpos;
			break;

		case SB_RIGHT:			// Scroll to far right.
			curpos = maxpos;
			break;

		case SB_ENDSCROLL:		// End scroll.
			m_container.Invalidate();
			break;

		case SB_LINELEFT:		// Scroll left.
			if(curpos > minpos)
				curpos--;
			break;

		case SB_LINERIGHT:		// Scroll right.
			if(curpos < maxpos)
				curpos++;
			break;

		case SB_PAGELEFT:		// Scroll one page left.
			if(curpos > minpos)
			{
				curpos = std::max(minpos, curpos - static_cast<int>(sbInfo.nPage));
			}
			break;

		case SB_PAGERIGHT:		// Scroll one page right.
			if(curpos < maxpos)
			{
				curpos = std::min(maxpos, curpos + static_cast<int>(sbInfo.nPage));
			}
			break;

		case SB_THUMBPOSITION:	// Scroll to absolute position. nPos is the position
			curpos = nPos;		// of the scroll box at the end of the drag operation.
			break;

		case SB_THUMBTRACK:		// Drag scroll box to specified position. nPos is the
			curpos = nPos;		// position that the scroll box has been dragged to.
			break;
		}

		// Set the new position of the thumb (scroll box).
		pScrollBar->SetScrollPos(curpos);

		m_container.ScrollWindowEx(0, m_scrollPos - curpos, nullptr, nullptr, nullptr, nullptr, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE);
		m_scrollPos = curpos;
	}

	CDialog::OnVScroll(nSBCode, nPos, pScrollBar);
}


// Scrollable container for the sliders
BEGIN_MESSAGE_MAP(CTempoSwingDlg::SliderContainer, CDialog)
	//{{AFX_MSG_MAP(CTempoSwingDlg::SliderContainer)
	ON_WM_HSCROLL()
	ON_NOTIFY_EX(TTN_NEEDTEXT, 0, &CTempoSwingDlg::SliderContainer::OnToolTipNotify)
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()


void CTempoSwingDlg::SliderContainer::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar *pScrollBar)
{
	if(m_parent.m_checkGroup.GetCheck() != BST_UNCHECKED)
	{
		// Edit groups
		size_t editedGroup = 0;
		int editedValue = reinterpret_cast<CSliderCtrl *>(pScrollBar)->GetPos();
		for(size_t i = 0; i < m_parent.m_controls.size(); i++)
		{
			if(m_parent.m_controls[i]->valueSlider.m_hWnd == pScrollBar->m_hWnd)
			{
				editedGroup = (i / m_parent.m_groupSize) % 2u;
				break;
			}
		}
		for(size_t i = 0; i < m_parent.m_controls.size(); i++)
		{
			if((i / m_parent.m_groupSize) % 2u == editedGroup)
			{
				m_parent.m_controls[i]->valueSlider.SetPos(editedValue);
			}
		}
	}

	for(size_t i = 0; i < m_parent.m_controls.size(); i++)
	{
		m_parent.m_tempoSwing[i] = m_parent.m_controls[i]->GetValue();
	}
	m_parent.m_tempoSwing.Normalize();
	// Apply preview
	if(m_parent.m_pattern == PATTERNINDEX_INVALID)
	{
		m_parent.m_sndFile.m_tempoSwing = m_parent.m_tempoSwing;
	} else
	{
		m_parent.m_sndFile.Patterns[m_parent.m_pattern].SetTempoSwing(m_parent.m_tempoSwing);
	}

	for(size_t i = 0; i < m_parent.m_tempoSwing.size(); i++)
	{
		TCHAR s[32];
		wsprintf(s, _T("%i%%"), Util::muldivr(m_parent.m_tempoSwing[i], 100, TempoSwing::Unity));
		m_parent.m_controls[i]->valueLabel.SetWindowText(s);
	}

	CStatic::OnHScroll(nSBCode, nPos, pScrollBar);
}


BOOL CTempoSwingDlg::SliderContainer::OnToolTipNotify(UINT, NMHDR *pNMHDR, LRESULT *)
{
	TOOLTIPTEXT *pTTT = (TOOLTIPTEXT*)pNMHDR;
	for(size_t i = 0; i < m_parent.m_controls.size(); i++)
	{
		if((HWND)pNMHDR->idFrom == m_parent.m_controls[i]->valueSlider.m_hWnd)
		{
			int32 val = Util::muldivr(m_parent.m_tempoSwing[i], 100, TempoSwing::Unity) - 100;
			wsprintf(pTTT->szText, _T("%s%d"), val > 0 ? _T("+") : _T(""), val);
			return TRUE;
		}
	}
	return FALSE;
}


OPENMPT_NAMESPACE_END