/*
 * MIDIMacroDialog.cpp
 * -------------------
 * Purpose: MIDI Macro 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 "../mptrack/Reporting.h"
#include "../common/mptStringBuffer.h"
#include "Mainfrm.h"
#include "Mptrack.h"
#include "resource.h"
#include "MIDIMacroDialog.h"
#include "../soundlib/MIDIEvents.h"
#include "../soundlib/plugins/PlugInterface.h"


OPENMPT_NAMESPACE_BEGIN


BEGIN_MESSAGE_MAP(CMidiMacroSetup, CDialog)
	ON_COMMAND(IDC_BUTTON1,			&CMidiMacroSetup::OnSetAsDefault)
	ON_COMMAND(IDC_BUTTON2,			&CMidiMacroSetup::OnResetCfg)
	ON_COMMAND(IDC_BUTTON3,			&CMidiMacroSetup::OnMacroHelp)
	ON_CBN_SELCHANGE(IDC_COMBO1,	&CMidiMacroSetup::OnSFxChanged)
	ON_CBN_SELCHANGE(IDC_COMBO2,	&CMidiMacroSetup::OnSFxPresetChanged)
	ON_CBN_SELCHANGE(IDC_COMBO3,	&CMidiMacroSetup::OnZxxPresetChanged)
	ON_CBN_SELCHANGE(IDC_COMBO4,	&CMidiMacroSetup::UpdateZxxSelection)
	ON_CBN_SELCHANGE(IDC_MACROPLUG, &CMidiMacroSetup::OnPlugChanged)
	ON_CBN_SELCHANGE(IDC_MACROPARAM,&CMidiMacroSetup::OnPlugParamChanged)
	ON_CBN_SELCHANGE(IDC_MACROCC,	&CMidiMacroSetup::OnCCChanged)
	ON_EN_CHANGE(IDC_EDIT1,			&CMidiMacroSetup::OnSFxEditChanged)
	ON_EN_CHANGE(IDC_EDIT2,			&CMidiMacroSetup::OnZxxEditChanged)
	ON_COMMAND_RANGE(ID_PLUGSELECT, ID_PLUGSELECT + kSFxMacros - 1, &CMidiMacroSetup::OnViewAllParams)
	ON_COMMAND_RANGE(ID_PLUGSELECT + kSFxMacros, ID_PLUGSELECT + kSFxMacros + kSFxMacros - 1, &CMidiMacroSetup::OnSetSFx)
END_MESSAGE_MAP()


void CMidiMacroSetup::DoDataExchange(CDataExchange* pDX)
{
	CDialog::DoDataExchange(pDX);
	DDX_Control(pDX, IDC_COMBO1,	m_CbnSFx);
	DDX_Control(pDX, IDC_COMBO2,	m_CbnSFxPreset);
	DDX_Control(pDX, IDC_COMBO3,	m_CbnZxxPreset);
	DDX_Control(pDX, IDC_COMBO4,	m_CbnZxx);
	DDX_Control(pDX, IDC_EDIT1,		m_EditSFx);
	DDX_Control(pDX, IDC_EDIT2,		m_EditZxx);
	DDX_Control(pDX, IDC_MACROPLUG, m_CbnMacroPlug);
	DDX_Control(pDX, IDC_MACROPARAM, m_CbnMacroParam);
	DDX_Control(pDX, IDC_MACROCC,   m_CbnMacroCC);
}


BOOL CMidiMacroSetup::OnInitDialog()
{
	CString s;
	CDialog::OnInitDialog();
	m_EditSFx.SetLimitText(kMacroLength - 1);
	m_EditZxx.SetLimitText(kMacroLength - 1);

	// Parametered macro selection
	for(int i = 0; i < 16; i++)
	{
		s.Format(_T("%d (SF%X)"), i, i);
		m_CbnSFx.AddString(s);
	}

	// Parametered macro presets
	m_CbnSFx.SetCurSel(0);
	for(int i = 0; i < kSFxMax; i++)
	{
		m_CbnSFxPreset.SetItemData(m_CbnSFxPreset.AddString(m_MidiCfg.GetParameteredMacroName(static_cast<ParameteredMacro>(i))), i);
	}
	OnSFxChanged();

	// MIDI CC selection box
	for (int cc = MIDIEvents::MIDICC_start; cc <= MIDIEvents::MIDICC_end; cc++)
	{
		s.Format(_T("CC %02d "), cc);
		s += mpt::ToCString(mpt::Charset::UTF8, MIDIEvents::MidiCCNames[cc]);
		m_CbnMacroCC.SetItemData(m_CbnMacroCC.AddString(s), cc);
	}

	// Z80...ZFF box
	for(int zxx = 0x80; zxx <= 0xFF; zxx++)
	{
		s.Format(_T("Z%02X"), zxx);
		m_CbnZxx.AddString(s);
	}

	// Fixed macro presets
	m_CbnZxx.SetCurSel(0);
	for(int i = 0; i < kZxxMax; i++)
	{
		m_CbnZxxPreset.SetItemData(m_CbnZxxPreset.AddString(m_MidiCfg.GetFixedMacroName(static_cast<FixedMacro>(i))), i);
	}
	m_CbnZxxPreset.SetCurSel(m_MidiCfg.GetFixedMacroType());

	UpdateDialog();

	auto ScalePixels = [&](auto x) { return Util::ScalePixels(x, m_hWnd); };
	int offsetx = ScalePixels(19), offsety = ScalePixels(30), separatorx = ScalePixels(4), separatory = ScalePixels(2);
	int height = ScalePixels(18), widthMacro = ScalePixels(30), widthVal = ScalePixels(179), widthType = ScalePixels(135), widthBtn = ScalePixels(70);

	for(UINT m = 0; m < kSFxMacros; m++)
	{
		m_EditMacro[m].Create(_T(""), WS_CHILD | WS_VISIBLE | WS_TABSTOP,
			CRect(offsetx, offsety + m * (separatory + height), offsetx + widthMacro, offsety + m * (separatory + height) + height), this, ID_PLUGSELECT + kSFxMacros + m);
		m_EditMacro[m].SetFont(GetFont());

		m_EditMacroType[m].Create(ES_READONLY | WS_CHILD| WS_VISIBLE | WS_TABSTOP | WS_BORDER, 
			CRect(offsetx + separatorx + widthMacro, offsety + m * (separatory + height), offsetx + widthMacro + widthType, offsety + m * (separatory + height) + height), this, ID_PLUGSELECT + kSFxMacros + m);
		m_EditMacroType[m].SetFont(GetFont());

		m_EditMacroValue[m].Create(ES_CENTER | ES_READONLY | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER, 
			CRect(offsetx + separatorx + widthType + widthMacro, offsety + m * (separatory + height), offsetx + widthMacro + widthType + widthVal, offsety + m * (separatory + height) + height), this, ID_PLUGSELECT + kSFxMacros + m);
		m_EditMacroValue[m].SetFont(GetFont());

		m_BtnMacroShowAll[m].Create(_T("Show All..."), WS_CHILD | WS_TABSTOP | WS_VISIBLE,
			CRect(offsetx + separatorx + widthType + widthMacro + widthVal, offsety + m * (separatory + height), offsetx + widthMacro + widthType + widthVal + widthBtn, offsety + m * (separatory + height) + height), this, ID_PLUGSELECT + m);
		m_BtnMacroShowAll[m].SetFont(GetFont());
	}
	UpdateMacroList();

#ifndef NO_PLUGINS
	for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++)
	{
		const SNDMIXPLUGIN &plugin = m_SndFile.m_MixPlugins[i];

		if(plugin.IsValidPlugin())
		{
			s.Format(_T("FX%d: "), i + 1);
			s += mpt::ToCString(plugin.GetName());
			m_CbnMacroPlug.SetItemData(m_CbnMacroPlug.AddString(s), i);
		}
	}
	m_CbnMacroPlug.SetCurSel(0);
	OnPlugChanged();
#endif  // NO_PLUGINS
	return FALSE;
}


// macro == -1 for updating all macros at once
void CMidiMacroSetup::UpdateMacroList(int macro)
{
	if(!m_EditMacro[0])
	{
		// GUI not yet initialized
		return;
	}

	int start, end;

	if(macro >= 0 && macro < kSFxMacros)
	{
		start = end = macro;
	} else
	{
		start = 0;
		end = kSFxMacros - 1;
	}

	CString s;
	const int selectedMacro = m_CbnSFx.GetCurSel();

	for(int m = start; m <= end; m++)
	{
		// SFx
		s.Format(_T("SF%X"), static_cast<unsigned int>(m));
		m_EditMacro[m].SetWindowText(s);

		// Macro value:
		m_EditMacroValue[m].SetWindowText(mpt::ToCString(mpt::Charset::ASCII, m_MidiCfg.SFx[m]));
		m_EditMacroValue[m].SetBackColor(m == selectedMacro ? RGB(200, 200, 225) : RGB(245, 245, 245));

		// Macro Type:
		const ParameteredMacro macroType = m_MidiCfg.GetParameteredMacroType(m);
		switch(macroType)
		{
		case kSFxPlugParam:
			s.Format(_T("Control Plugin Param %u"), static_cast<unsigned int>(m_MidiCfg.MacroToPlugParam(m)));
			break;

		default:
			s = m_MidiCfg.GetParameteredMacroName(m);
			break;
		}
		m_EditMacroType[m].SetWindowText(s);
		m_EditMacroType[m].SetBackColor(m == selectedMacro ? RGB(200,200,225) : RGB(245,245,245));

		// Param details button:
		m_BtnMacroShowAll[m].ShowWindow((macroType == kSFxPlugParam) ? SW_SHOW : SW_HIDE);
	}
}


void CMidiMacroSetup::UpdateDialog()
{
	UINT sfx = m_CbnSFx.GetCurSel();
	UINT sfx_preset = static_cast<UINT>(m_CbnSFxPreset.GetItemData(m_CbnSFxPreset.GetCurSel()));
	if(sfx < m_MidiCfg.SFx.size())
	{
		ToggleBoxes(sfx_preset, sfx);
		m_EditSFx.SetWindowText(mpt::ToCString(mpt::Charset::ASCII, m_MidiCfg.SFx[sfx]));
	}

	UpdateZxxSelection();
	UpdateMacroList();
}


void CMidiMacroSetup::OnSetAsDefault()
{
	theApp.SetDefaultMidiMacro(m_MidiCfg);
}


void CMidiMacroSetup::OnResetCfg()
{
	theApp.GetDefaultMidiMacro(m_MidiCfg);
	m_CbnZxxPreset.SetCurSel(0);
	OnSFxChanged();
}


void CMidiMacroSetup::OnMacroHelp()
{
	Reporting::Information(_T("Valid characters in macros:\n\n"
		"0-9, A-F - Raw hex data (4-Bit value)\n"
		"c - MIDI channel (4-Bit value)\n"
		"n - Note value\n\n"
		"v - Note velocity\n"
		"u - Computed note volume (including envelopes)\n\n"
		"x - Note panning\n"
		"y - Computed panning (including envelopes)\n\n"
		"a - High byte of bank select\n"
		"b - Low byte of bank select\n"
		"p - Program select\n\n"
		"h - Pattern channel\n"
		"m - Sample loop direction\n"
		"o - Last sample offset (Oxx / 9xx)\n"
		"s - SysEx checksum (Roland)\n\n"
		"z - Zxx parameter (00-7F)\n\n"
		"Macros can be up to 31 characters long and contain multiple MIDI messages. SysEx messages are automatically terminated if not specified by the user."),
		_T("OpenMPT MIDI Macro quick reference"));
}


void CMidiMacroSetup::OnSFxChanged()
{
	UINT sfx = m_CbnSFx.GetCurSel();
	if (sfx < 16)
	{
		int preset = m_MidiCfg.GetParameteredMacroType(sfx);
		m_CbnSFxPreset.SetCurSel(preset);
	}
	UpdateDialog();
}


void CMidiMacroSetup::OnSFxPresetChanged()
{
	UINT sfx = m_CbnSFx.GetCurSel();
	ParameteredMacro sfx_preset = static_cast<ParameteredMacro>(m_CbnSFxPreset.GetItemData(m_CbnSFxPreset.GetCurSel()));

	if (sfx < kSFxMacros)
	{
		if(sfx_preset != kSFxCustom)
		{
			m_MidiCfg.CreateParameteredMacro(sfx, sfx_preset);
		}
		UpdateDialog();
	}
}


void CMidiMacroSetup::OnZxxPresetChanged()
{
	FixedMacro zxxPreset = static_cast<FixedMacro>(m_CbnZxxPreset.GetItemData(m_CbnZxxPreset.GetCurSel()));

	if (zxxPreset != kZxxCustom)
	{
		m_MidiCfg.CreateFixedMacro(zxxPreset);
		UpdateDialog();
	}
}


void CMidiMacroSetup::UpdateZxxSelection()
{
	UINT zxx = m_CbnZxx.GetCurSel();
	if(zxx < m_MidiCfg.Zxx.size())
	{
		m_EditZxx.SetWindowText(mpt::ToCString(mpt::Charset::ASCII, m_MidiCfg.Zxx[zxx]));
	}
}


void CMidiMacroSetup::OnSFxEditChanged()
{
	UINT sfx = m_CbnSFx.GetCurSel();
	if(sfx < m_MidiCfg.SFx.size())
	{
		if(ValidateMacroString(m_EditSFx, m_MidiCfg.SFx[sfx], true))
		{
			CString s;
			m_EditSFx.GetWindowText(s);
			m_MidiCfg.SFx[sfx] = mpt::ToCharset(mpt::Charset::ASCII, s);

			int sfx_preset = m_MidiCfg.GetParameteredMacroType(sfx);
			m_CbnSFxPreset.SetCurSel(sfx_preset);
			ToggleBoxes(sfx_preset, sfx);
			UpdateMacroList(sfx);
		}
	}
}


void CMidiMacroSetup::OnZxxEditChanged()
{
	UINT zxx = m_CbnZxx.GetCurSel();
	if(zxx < m_MidiCfg.Zxx.size())
	{
		if(ValidateMacroString(m_EditZxx, m_MidiCfg.Zxx[zxx], false))
		{
			CString s;
			m_EditZxx.GetWindowText(s);
			m_MidiCfg.Zxx[zxx] = mpt::ToCharset(mpt::Charset::ASCII, s);
			m_CbnZxxPreset.SetCurSel(m_MidiCfg.GetFixedMacroType());
		}
	}
}

void CMidiMacroSetup::OnSetSFx(UINT id)
{
	m_CbnSFx.SetCurSel(id - (ID_PLUGSELECT + kSFxMacros));
	OnSFxChanged();
}

void CMidiMacroSetup::OnViewAllParams(UINT id)
{
#ifndef NO_PLUGINS
	CString message, plugName;
	int sfx = id - ID_PLUGSELECT;
	PlugParamIndex param = m_MidiCfg.MacroToPlugParam(sfx);
	message.Format(_T("These are the parameters that can be controlled by macro SF%X:\n\n"), sfx);

	for(PLUGINDEX plug = 0; plug < MAX_MIXPLUGINS; plug++)
	{
		IMixPlugin *pVstPlugin = m_SndFile.m_MixPlugins[plug].pMixPlugin;
		if(pVstPlugin && param < pVstPlugin->GetNumParameters())
		{
			plugName = mpt::ToCString(m_SndFile.m_MixPlugins[plug].GetName());
			message.AppendFormat(_T("FX%d: "), plug + 1);
			message += plugName + _T("\t") + pVstPlugin->GetFormattedParamName(param) + _T("\n");
		}
	}

	Reporting::Notification(message, _T("Macro -> Parameters"));
#endif // NO_PLUGINS
}

void CMidiMacroSetup::OnPlugChanged()
{
#ifndef NO_PLUGINS
	DWORD_PTR plug = m_CbnMacroPlug.GetItemData(m_CbnMacroPlug.GetCurSel());

	if(plug >= MAX_MIXPLUGINS)
		return;

	IMixPlugin *pVstPlugin = m_SndFile.m_MixPlugins[plug].pMixPlugin;
	if (pVstPlugin != nullptr)
	{
		m_CbnMacroParam.SetRedraw(FALSE);
		m_CbnMacroParam.Clear();
		m_CbnMacroParam.ResetContent();
		AddPluginParameternamesToCombobox(m_CbnMacroParam, *pVstPlugin);
		m_CbnMacroParam.SetRedraw(TRUE);

		int param = m_MidiCfg.MacroToPlugParam(m_CbnSFx.GetCurSel());
		m_CbnMacroParam.SetCurSel(param);
	}
#endif // NO_PLUGINS
}

void CMidiMacroSetup::OnPlugParamChanged()
{
	int param = static_cast<int>(m_CbnMacroParam.GetItemData(m_CbnMacroParam.GetCurSel()));

	if(param < 384)
	{
		const std::string macroText = m_MidiCfg.CreateParameteredMacro(kSFxPlugParam, param);
		m_EditSFx.SetWindowText(mpt::ToCString(mpt::Charset::ASCII, macroText));
	} else
	{
		Reporting::Notification("Only parameters 0 to 383 can be controlled using MIDI Macros. Use Parameter Control Events to automate higher parameters.");
	}
}

void CMidiMacroSetup::OnCCChanged()
{
	int cc = static_cast<int>(m_CbnMacroCC.GetItemData(m_CbnMacroCC.GetCurSel()));
	const std::string macroText = m_MidiCfg.CreateParameteredMacro(kSFxCC, cc);
	m_EditSFx.SetWindowText(mpt::ToCString(mpt::Charset::ASCII, macroText));
}

void CMidiMacroSetup::ToggleBoxes(UINT sfxPreset, UINT sfx)
{

	if (sfxPreset == kSFxPlugParam)
	{
		m_CbnMacroCC.ShowWindow(FALSE);
		m_CbnMacroPlug.ShowWindow(TRUE);
		m_CbnMacroParam.ShowWindow(TRUE);
		m_CbnMacroPlug.EnableWindow(TRUE);
		m_CbnMacroParam.EnableWindow(TRUE);
		SetDlgItemText(IDC_GENMACROLABEL, _T("Plugin/Param"));
		m_CbnMacroParam.SetCurSel(m_MidiCfg.MacroToPlugParam(sfx));
	} else
	{
		m_CbnMacroPlug.EnableWindow(FALSE);
		m_CbnMacroParam.EnableWindow(FALSE);
	}

	if (sfxPreset == kSFxCC)
	{
		m_CbnMacroCC.EnableWindow(TRUE);
		m_CbnMacroCC.ShowWindow(TRUE);
		m_CbnMacroPlug.ShowWindow(FALSE);
		m_CbnMacroParam.ShowWindow(FALSE);
		SetDlgItemText(IDC_GENMACROLABEL, _T("MIDI CC"));
		m_CbnMacroCC.SetCurSel(m_MidiCfg.MacroToMidiCC(sfx));
	} else
	{
		m_CbnMacroCC.EnableWindow(FALSE);
	}
}


bool CMidiMacroSetup::ValidateMacroString(CEdit &wnd, const MIDIMacroConfig::Macro &prevMacro, bool isParametric)
{
	CString macroStrT;
	wnd.GetWindowText(macroStrT);
	std::string macroStr = mpt::ToCharset(mpt::Charset::ASCII, macroStrT);

	bool allowed = true, caseChange = false;
	for(char &c : macroStr)
	{
		if(c == 'k' || c == 'K')  // Previously, 'K' was used for MIDI channel
		{
			caseChange = true;
			c = 'c';
		} else if(c >= 'd' && c <= 'f')  // abc have special meanings, but def can be fixed
		{
			caseChange = true;
			c = c - 'a' + 'A';
		} else if(c == 'M' || c == 'N' || c == 'O' || c == 'P' || c == 'S' || c == 'U' || c == 'V' || c == 'X' || c == 'Y' || c == 'Z')
		{
			caseChange = true;
			c = c - 'A' + 'a';
		} else if(!(
			(c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'c') ||
			(c == 'h' || c == 'm' || c == 'n' || c == 'o' || c == 'p' || c == 's' ||c == 'u' || c == 'v' || c == 'x' || c == 'y' || c == ' ') ||
			(c == 'z' && isParametric)))
		{
			allowed = false;
			break;
		}
	}

	if(!allowed)
	{
		// Replace text and keep cursor position if we just typed in an invalid character
		if(prevMacro != std::string_view{macroStr})
		{
			int start, end;
			wnd.GetSel(start, end);
			wnd.SetWindowText(mpt::ToCString(mpt::Charset::ASCII, prevMacro));
			wnd.SetSel(start - 1, end - 1, true);
			MessageBeep(MB_OK);
		}
		return false;
	} else
	{
		if(caseChange)
		{
			// Replace text and keep cursor position if there was a case conversion
			int start, end;
			wnd.GetSel(start, end);
			wnd.SetWindowText(mpt::ToCString(mpt::Charset::ASCII, macroStr));
			wnd.SetSel(start, end, true);
		}
		return true;
	}
}


OPENMPT_NAMESPACE_END