/*
 * PatternClipboard.cpp
 * --------------------
 * Purpose: Implementation of the pattern clipboard mechanism
 * 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 "PatternClipboard.h"
#include "PatternCursor.h"
#include "Mainfrm.h"
#include "Moddoc.h"
#include "Clipboard.h"
#include "View_pat.h"
#include "../soundlib/mod_specifications.h"
#include "../soundlib/Tables.h"


OPENMPT_NAMESPACE_BEGIN


/* Clipboard format:
 * Hdr: "ModPlug Tracker S3M\r\n"
 * Full:  '|C#401v64A06'
 * Reset: '|...........'
 * Empty: '|           '
 * End of row: '\r\n'
 *
 * When pasting multiple patterns, the header line is followed by the order list:
 * Orders: 0,1,2,+,-,1\r\n
 * After that, the individual pattern headers and pattern data follows:
 * 'Rows: 64\r\n' (must be first)
 * 'Name: Pattern Name\r\n' (optional)
 * 'Signature: 4/16\r\n' (optional)
 * 'Swing: 16777216,16777216,16777216,16777216\r\n' (optional)
 * Pattern data...
 */

PatternClipboard PatternClipboard::instance;

std::string PatternClipboard::GetFileExtension(const char *ext, bool addPadding)
{
	std::string format(ext);
	if(format.size() > 3)
	{
		format.resize(3);
	}
	format = mpt::ToUpperCaseAscii(format);
	if(addPadding)
	{
		format.insert(0, 3 - format.size(), ' ');
	}
	return format;
}


std::string PatternClipboard::FormatClipboardHeader(const CSoundFile &sndFile)
{
	return "ModPlug Tracker " + GetFileExtension(sndFile.GetModSpecifications().fileExtension, true) + "\r\n";
}


// Copy a range of patterns to both the system clipboard and the internal clipboard.
bool PatternClipboard::Copy(const CSoundFile &sndFile, ORDERINDEX first, ORDERINDEX last, bool onlyOrders)
{
	const ModSequence &order = sndFile.Order();
	LimitMax(first, order.GetLength());
	LimitMax(last, order.GetLength());

	// Set up clipboard header.
	std::string data = FormatClipboardHeader(sndFile) + "Orders: ";
	std::string patternData;

	// Pattern => Order list assignment
	std::vector<PATTERNINDEX> patList(sndFile.Patterns.Size(), PATTERNINDEX_INVALID);
	PATTERNINDEX insertedPats = 0;

	// Add order list and pattern information to header.
	for(ORDERINDEX ord = first; ord <= last; ord++)
	{
		PATTERNINDEX pattern = order[ord];

		if(ord != first)
			data += ',';
		
		if(pattern == order.GetInvalidPatIndex())
		{
			data += '-';
		} else if(pattern == order.GetIgnoreIndex())
		{
			data += '+';
		} else if(sndFile.Patterns.IsValidPat(pattern))
		{
			if(onlyOrders)
			{
				patList[pattern] = pattern;
			} else if(patList[pattern] == PATTERNINDEX_INVALID)
			{
				// New pattern
				patList[pattern] = insertedPats++;

				const CPattern &pat = sndFile.Patterns[pattern];
				patternData += MPT_AFORMAT("Rows: {}\r\n")(pat.GetNumRows());
				std::string name = pat.GetName();
				if(!name.empty())
				{
					patternData += "Name: " + name + "\r\n";
				}
				if(pat.GetOverrideSignature())
				{
					patternData += MPT_AFORMAT("Signature: {}/{}\r\n")(pat.GetRowsPerBeat(), pat.GetRowsPerMeasure());
				}
				if(pat.HasTempoSwing())
				{
					patternData += "Swing: ";
					const TempoSwing &swing = pat.GetTempoSwing();
					for(size_t i = 0; i < swing.size(); i++)
					{
						if(i == 0)
						{
							patternData += MPT_AFORMAT("{}")(swing[i]);
						} else
						{
							patternData += MPT_AFORMAT(",{}")(swing[i]);
						}
					}
					patternData += "\r\n";
				}
				patternData += CreateClipboardString(sndFile, pattern, PatternRect(PatternCursor(), PatternCursor(sndFile.Patterns[pattern].GetNumRows() - 1, sndFile.GetNumChannels() - 1, PatternCursor::lastColumn)));
			}

			data += mpt::afmt::val(patList[pattern]);
		}
	}
	if(!onlyOrders)
	{
		data += "\r\n" + patternData;
	}
	
	if(instance.m_activeClipboard < instance.m_clipboards.size())
	{
		// Copy to internal clipboard
		CString desc = MPT_CFORMAT("{} {} ({} to {})")(last - first + 1, onlyOrders ? CString(_T("Orders")) : CString(_T("Patterns")), first, last);
		instance.m_clipboards[instance.m_activeClipboard] = {data, desc};
	}

	return ToSystemClipboard(data);
}


// Copy a pattern selection to both the system clipboard and the internal clipboard.
bool PatternClipboard::Copy(const CSoundFile &sndFile, PATTERNINDEX pattern, PatternRect selection)
{
	std::string data = CreateClipboardString(sndFile, pattern, selection);
	if(data.empty())
		return false;

	// Set up clipboard header
	data.insert(0, FormatClipboardHeader(sndFile));

	if(instance.m_activeClipboard < instance.m_clipboards.size())
	{
		// Copy to internal clipboard
		CString desc;
		desc.Format(_T("%u rows, %u channels (pattern %u)"), selection.GetNumRows(), selection.GetNumChannels(), pattern);
		instance.m_clipboards[instance.m_activeClipboard] = {data, desc};
	}

	return ToSystemClipboard(data);
}


// Copy a pattern or pattern channel to the internal pattern or channel clipboard.
bool PatternClipboard::Copy(const CSoundFile &sndFile, PATTERNINDEX pattern, CHANNELINDEX channel)
{
	if(!sndFile.Patterns.IsValidPat(pattern))
		return false;

	const bool patternCopy = (channel == CHANNELINDEX_INVALID);
	const CPattern &pat = sndFile.Patterns[pattern];
	PatternRect selection;
	if(patternCopy)
		selection = {PatternCursor(0, 0, PatternCursor::firstColumn), PatternCursor(pat.GetNumRows() - 1, pat.GetNumChannels() - 1, PatternCursor::lastColumn)};
	else
		selection = {PatternCursor(0, channel, PatternCursor::firstColumn), PatternCursor(pat.GetNumRows() - 1, channel, PatternCursor::lastColumn)};

	std::string data = CreateClipboardString(sndFile, pattern, selection);
	if(data.empty())
		return false;

	// Set up clipboard header
	data.insert(0, FormatClipboardHeader(sndFile));

	// Copy to internal clipboard
	(patternCopy ? instance.m_patternClipboard : instance.m_channelClipboard) = {data, {}};
	return true;
}


// Create the clipboard text for a pattern selection
std::string PatternClipboard::CreateClipboardString(const CSoundFile &sndFile, PATTERNINDEX pattern, PatternRect selection)
{
	if(!sndFile.Patterns.IsValidPat(pattern))
		return "";

	if(selection.GetStartColumn() == PatternCursor::paramColumn)
	{
		// Special case: If selection starts with a parameter column, extend it to include the effect letter as well.
		PatternCursor upper(selection.GetUpperLeft());
		upper.Move(0, 0, -1);
		selection = PatternRect(upper, selection.GetLowerRight());
	}

	const ROWINDEX startRow = selection.GetStartRow(), numRows = selection.GetNumRows();
	const CHANNELINDEX startChan = selection.GetStartChannel(), numChans = selection.GetNumChannels();

	std::string data;
	data.reserve(numRows * (numChans * 12 + 2));

	for(ROWINDEX row = 0; row < numRows; row++)
	{
		if(row + startRow >= sndFile.Patterns[pattern].GetNumRows())
			break;

		const ModCommand *m = sndFile.Patterns[pattern].GetpModCommand(row + startRow, startChan);

		for(CHANNELINDEX chn = 0; chn < numChans; chn++, m++)
		{
			PatternCursor cursor(0, startChan + chn);
			data += '|';

			// Note
			if(selection.ContainsHorizontal(cursor))
			{
				if(m->IsNote())
				{
					// Need to guarantee that sharps are used for the clipboard.
					data += mpt::ToCharset(mpt::Charset::Locale, mpt::ustring(NoteNamesSharp[(m->note - NOTE_MIN) % 12]));
					data += ('0' + (m->note - NOTE_MIN) / 12);
				} else
				{
					data += mpt::ToCharset(mpt::Charset::Locale, sndFile.GetNoteName(m->note));
				}
			} else
			{
				// No note
				data += "   ";
			}

			// Instrument
			cursor.Move(0, 0, 1);
			if(selection.ContainsHorizontal(cursor))
			{
				if(m->instr)
				{
					data += ('0' + (m->instr / 10));
					data += ('0' + (m->instr % 10));
				} else
				{
					data += "..";
				}
			} else
			{
				data += "  ";
			}

			// Volume
			cursor.Move(0, 0, 1);
			if(selection.ContainsHorizontal(cursor))
			{
				if(m->IsPcNote())
				{
					data += mpt::afmt::dec0<3>(m->GetValueVolCol());
				}
				else
				{
					if(m->volcmd != VOLCMD_NONE && m->vol <= 99)
					{
						data += sndFile.GetModSpecifications().GetVolEffectLetter(m->volcmd);
						data += mpt::afmt::dec0<2>(m->vol);
					} else
					{
						data += "...";
					}
				}
			} else
			{
				data += "   ";
			}
			
			// Effect
			cursor.Move(0, 0, 1);
			if(selection.ContainsHorizontal(cursor))
			{
				if(m->IsPcNote())
				{
					data += mpt::afmt::dec0<3>(m->GetValueEffectCol());
				}
				else
				{
					if(m->command != CMD_NONE)
					{
						data += sndFile.GetModSpecifications().GetEffectLetter(m->command);
					} else
					{
						data += '.';
					}

					if(m->param != 0 && m->command != CMD_NONE)
					{
						data += mpt::afmt::HEX0<2>(m->param);
					} else
					{
						data += "..";
					}
				}
			} else
			{
				data += "   ";
			}
		}

		// Next Row
		data += "\r\n";
	}

	return data;
}


// Try pasting a pattern selection from the system clipboard.
bool PatternClipboard::Paste(CSoundFile &sndFile, PatternEditPos &pastePos, PasteModes mode, PatternRect &pasteRect, bool &orderChanged)
{
	std::string data;
	if(!FromSystemClipboard(data) || !HandlePaste(sndFile, pastePos, mode, data, pasteRect, orderChanged))
	{
		// Fall back to internal clipboard if there's no valid pattern data in the system clipboard.
		return Paste(sndFile, pastePos, mode, pasteRect, instance.m_activeClipboard, orderChanged);
	}
	return true;
}


// Try pasting a pattern selection from an internal clipboard.
bool PatternClipboard::Paste(CSoundFile &sndFile, PatternEditPos &pastePos, PasteModes mode, PatternRect &pasteRect, clipindex_t internalClipboard, bool &orderChanged)
{
	if(internalClipboard >= instance.m_clipboards.size())
		return false;
	
	return HandlePaste(sndFile, pastePos, mode, instance.m_clipboards[internalClipboard].content, pasteRect, orderChanged);
}


// Paste from pattern or channel clipboard.
bool PatternClipboard::Paste(CSoundFile &sndFile, PATTERNINDEX pattern, CHANNELINDEX channel)
{
	PatternEditPos pastePos{0, ORDERINDEX_INVALID, pattern, channel != CHANNELINDEX_INVALID ? channel : CHANNELINDEX(0)};
	PatternRect pasteRect;
	bool orderChanged = false;
	return HandlePaste(sndFile, pastePos, pmOverwrite, (channel == CHANNELINDEX_INVALID ? instance.m_patternClipboard : instance.m_channelClipboard).content, pasteRect, orderChanged);
}


// Parse clipboard string and perform the pasting operation.
bool PatternClipboard::HandlePaste(CSoundFile &sndFile, PatternEditPos &pastePos, PasteModes mode, const std::string &data, PatternRect &pasteRect, bool &orderChanged)
{
	const std::string whitespace(" \n\r\t");
	PATTERNINDEX pattern = pastePos.pattern;
	ORDERINDEX &curOrder = pastePos.order;
	orderChanged = false;
	if(sndFile.GetpModDoc() == nullptr)
		return false;

	CModDoc &modDoc = *(sndFile.GetpModDoc());
	ModSequence &order = sndFile.Order();

	bool success = false;
	bool prepareUndo = true;	// Prepare pattern for undo next time
	bool firstUndo = true;		// For chaining undos (see overflow / multi-pattern paste)

	// Search for signature
	std::string::size_type pos, startPos = 0;
	MODTYPE pasteFormat = MOD_TYPE_NONE;
	while(pasteFormat == MOD_TYPE_NONE && (startPos = data.find("ModPlug Tracker ", startPos)) != std::string::npos)
	{
		startPos += 16;
		// Check paste format
		const std::string format = mpt::ToUpperCaseAscii(mpt::trim(data.substr(startPos, 3)));

		for(const auto &spec : ModSpecs::Collection)
		{
			if(format == GetFileExtension(spec->fileExtension, false))
			{
				pasteFormat = spec->internalType;
				startPos += 3;
				break;
			}
		}
	}

	// What is this I don't even
	if(startPos == std::string::npos)
		return false;
	// Skip whitespaces
	startPos = data.find_first_not_of(whitespace, startPos);
	if(startPos == std::string::npos)
		return false;
	
	// Multi-order stuff
	std::vector<PATTERNINDEX> patList;
	// Multi-order mix-paste stuff
	std::vector<ORDERINDEX> ordList;
	std::vector<std::string::size_type> patOffset;

	enum { kSinglePaste, kMultiInsert, kMultiOverwrite } patternMode = kSinglePaste;
	if(data.substr(startPos, 8) == "Orders: ")
	{
		// Pasting several patterns at once.
		patternMode = (mode == pmOverwrite) ?  kMultiInsert : kMultiOverwrite;

		// Put new patterns after current pattern, if it exists
		if(order.IsValidPat(curOrder) && patternMode == kMultiInsert)
			curOrder++;

		pos = startPos + 8;
		startPos = data.find('\n', pos);
		ORDERINDEX writeOrder = curOrder;
		const bool onlyOrders = (startPos == std::string::npos);
		if(onlyOrders)
		{
			// Only create order list, no patterns
			startPos = data.size();
		} else
		{
			startPos++;
		}

		while(pos < startPos && pos != std::string::npos)
		{
			PATTERNINDEX insertPat;
			auto curPos = pos;
			// Next order item, please
			pos = data.find(',', pos + 1);
			if(pos != std::string::npos)
				pos++;

			if(data[curPos] == '+')
			{
				insertPat = order.GetIgnoreIndex();
			} else if(data[curPos] == '-')
			{
				insertPat = order.GetInvalidPatIndex();
			} else
			{
				insertPat = ConvertStrTo<PATTERNINDEX>(data.substr(curPos, 10));
				if(patternMode == kMultiOverwrite)
				{
					// We only want the order of pasted patterns now, do not create any new patterns
					ordList.push_back(insertPat);
					continue;
				}

				if(insertPat < patList.size() && patList[insertPat] != PATTERNINDEX_INVALID)
				{
					// Duplicate pattern
					insertPat = patList[insertPat];
				} else if(!onlyOrders)
				{
					// New pattern
					if(insertPat >= patList.size())
					{
						patList.resize(insertPat + 1, PATTERNINDEX_INVALID);
					}

					patList[insertPat] = modDoc.InsertPattern(64);
					insertPat = patList[insertPat];
				}
			}

			if((insertPat == order.GetIgnoreIndex() && !sndFile.GetModSpecifications().hasIgnoreIndex)
				|| (insertPat == order.GetInvalidPatIndex() && !sndFile.GetModSpecifications().hasStopIndex)
				|| insertPat == PATTERNINDEX_INVALID
				|| patternMode == kMultiOverwrite)
			{
				continue;
			}

			if(order.insert(writeOrder, 1) == 0)
			{
				break;
			}
			order[writeOrder++] = insertPat;
			orderChanged = true;
		}

		if(patternMode == kMultiInsert)
		{
			if(!patList.empty())
			{
				// First pattern we're going to paste in.
				pattern = patList[0];
			}

			// We already modified the order list...
			success = true;
			pastePos.pattern = pattern;
			pastePos.row = 0;
			pastePos.channel = 0;
		} else
		{
			if(ordList.empty())
				return success;
			// Find pattern offsets
			pos = startPos;
			patOffset.reserve(ordList.size());
			bool patStart = false;
			while((pos = data.find_first_not_of(whitespace, pos)) != std::string::npos)
			{
				auto eol = data.find('\n', pos + 1);
				if(eol == std::string::npos)
					eol = data.size();
				if(data.substr(pos, 6) == "Rows: ")
				{
					patStart = true;
				} else if(data.substr(pos, 1) == "|" && patStart)
				{
					patOffset.push_back(pos);
					patStart = false;
				}
				pos = eol;
			}
			if(patOffset.empty())
				return success;
			startPos = patOffset[0];
		}
	}

	size_t curPattern = 0;	// Currently pasted pattern for multi-paste
	ROWINDEX startRow = pastePos.row;
	ROWINDEX curRow = startRow;
	CHANNELINDEX startChan = pastePos.channel, col;

	// Can we actually paste at this position?
	if(!sndFile.Patterns.IsValidPat(pattern) || startRow >= sndFile.Patterns[pattern].GetNumRows() || startChan >= sndFile.GetNumChannels())
	{
		return success;
	}

	const CModSpecifications &sourceSpecs = CSoundFile::GetModSpecifications(pasteFormat);
	const bool overflowPaste = (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OVERFLOWPASTE) && mode != pmPasteFlood && mode != pmPushForward && patternMode != kMultiInsert && curOrder != ORDERINDEX_INVALID;
	const bool doITStyleMix = (mode == pmMixPasteIT);
	const bool doMixPaste = (mode == pmMixPaste) || doITStyleMix;
	const bool clipboardHasS3MCommands = (pasteFormat & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_S3M));
	const bool insertNewPatterns = overflowPaste && (patternMode == kMultiOverwrite);

	PatternCursor startPoint(startRow, startChan, PatternCursor::lastColumn), endPoint(startRow, startChan, PatternCursor::firstColumn);
	ModCommand *patData = sndFile.Patterns[pattern].GetpModCommand(startRow, 0);

	auto multiPastePos = ordList.cbegin();
	pos = startPos;

	while(curRow < sndFile.Patterns[pattern].GetNumRows() || overflowPaste || patternMode == kMultiInsert)
	{
		// Parse next line
		pos = data.find_first_not_of(whitespace, pos);
		if(pos == std::string::npos)
		{
			// End of paste
			if(mode == pmPasteFlood && curRow != startRow && curRow < sndFile.Patterns[pattern].GetNumRows())
			{
				// Restarting pasting from beginning.
				pos = startPos;
				multiPastePos = ordList.cbegin();
				continue;
			} else
			{
				// Prevent infinite loop with malformed clipboard data.
				break;
			}
		}
		auto eol = data.find('\n', pos + 1);
		if(eol == std::string::npos)
			eol = data.size();

		// Handle multi-paste: Read pattern information
		if(patternMode != kSinglePaste)
		{
			// Parse pattern header lines
			bool parsedLine = true;
			if(data.substr(pos, 6) == "Rows: ")
			{
				pos += 6;
				// Advance to next pattern
				if(patternMode == kMultiOverwrite)
				{
					// In case of multi-pattern mix-paste, we know that we reached the end of the previous pattern and need to parse the next order now.
					multiPastePos++;
					if(multiPastePos == ordList.cend() || *multiPastePos >= patOffset.size())
						pos = data.size();
					else
						pos = patOffset[*multiPastePos];
					continue;
				}

				// Otherwise, parse this pattern header normally.
				do
				{
					if(curPattern >= patList.size())
					{
						return success;
					}
					pattern = patList[curPattern++];
				} while (pattern == PATTERNINDEX_INVALID);
				ROWINDEX numRows = ConvertStrTo<ROWINDEX>(data.substr(pos, 10));
				sndFile.Patterns[pattern].Resize(numRows);
				patData = sndFile.Patterns[pattern].GetpModCommand(0, 0);
				curRow = 0;
				prepareUndo = true;
			} else if(data.substr(pos, 6) == "Name: ")
			{
				pos += 6;
				auto name = mpt::trim_right(data.substr(pos, eol - pos - 1));
				sndFile.Patterns[pattern].SetName(name);
			} else if(data.substr(pos, 11) == "Signature: ")
			{
				pos += 11;
				auto pos2 = data.find("/", pos + 1);
				if(pos2 != std::string::npos)
				{
					pos2++;
					ROWINDEX rpb = ConvertStrTo<ROWINDEX>(data.substr(pos, pos2 - pos));
					ROWINDEX rpm = ConvertStrTo<ROWINDEX>(data.substr(pos2, eol - pos2));
					sndFile.Patterns[pattern].SetSignature(rpb, rpm);
				}
			} else if(data.substr(pos, 7) == "Swing: ")
			{
				pos += 7;
				TempoSwing swing;
				swing.resize(sndFile.Patterns[pattern].GetRowsPerBeat(), TempoSwing::Unity);
				size_t i = 0;
				while(pos != std::string::npos && pos < eol && i < swing.size())
				{
					swing[i++] = ConvertStrTo<TempoSwing::value_type>(data.substr(pos, eol - pos));
					pos = data.find(',', pos + 1);
					if(pos != std::string::npos)
						pos++;
				}
				sndFile.Patterns[pattern].SetTempoSwing(swing);
			} else
			{
				parsedLine = false;
			}
			if(parsedLine)
			{
				pos = eol;
				continue;
			}
		}
		if(data[pos] != '|')
		{
			// Not a valid line?
			pos = eol;
			continue;
		}

		if(overflowPaste)
		{
			// Handle overflow paste. Continue pasting in next pattern if enabled.
			// If Paste Flood is enabled, this won't be called due to obvious reasons.
			while(curRow >= sndFile.Patterns[pattern].GetNumRows())
			{
				curRow = 0;
				ORDERINDEX nextOrder = order.GetNextOrderIgnoringSkips(curOrder);
				if(nextOrder <= curOrder || !order.IsValidPat(nextOrder))
				{
					PATTERNINDEX newPat;
					if(!insertNewPatterns
						|| curOrder >= sndFile.GetModSpecifications().ordersMax
						|| (newPat = modDoc.InsertPattern(sndFile.Patterns[pattern].GetNumRows())) == PATTERNINDEX_INVALID
						|| order.insert(curOrder + 1, 1, newPat) == 0)
					{
						return success;
					}
					orderChanged = true;
					nextOrder = curOrder + 1;
				}
				pattern = order[nextOrder];
				if(!sndFile.Patterns.IsValidPat(pattern)) return success;
				patData = sndFile.Patterns[pattern].GetpModCommand(0, 0);
				curOrder = nextOrder;
				prepareUndo = true;
				startRow = 0;
			}
		}

		success = true;
		col = startChan;
		// Paste columns
		while((pos + 11 < data.size()) && (data[pos] == '|'))
		{
			pos++;
			// Handle pasting large pattern into smaller pattern (e.g. 128-row pattern into MOD, which only allows 64 rows)
			ModCommand dummy;
			ModCommand &m = curRow < sndFile.Patterns[pattern].GetNumRows() ? patData[col] : dummy;

			// Check valid paste condition. Paste will be skipped if
			// - col is not a valid channelindex or
			// - doing mix paste and paste destination modcommand is a PCnote or
			// - doing mix paste and trying to paste PCnote on non-empty modcommand.
			const bool skipPaste =
				col >= sndFile.GetNumChannels() ||
				(doMixPaste && m.IsPcNote()) ||
				(doMixPaste && data[pos] == 'P' && !m.IsEmpty());

			if(skipPaste == false)
			{
				// Before changing anything in this pattern, we have to create an undo point.
				if(prepareUndo)
				{
					modDoc.GetPatternUndo().PrepareUndo(pattern, startChan, startRow, sndFile.GetNumChannels(), sndFile.Patterns[pattern].GetNumRows(), "Paste", !firstUndo);
					prepareUndo = false;
					firstUndo = false;
				}

				// ITSyle mixpaste requires that we keep a copy of the thing we are about to paste on
				// so that we can refer back to check if there was anything in e.g. the note column before we pasted.
				const ModCommand origModCmd = m;

				// push channel data below paste point first.
				if(mode == pmPushForward)
				{
					for(ROWINDEX pushRow = sndFile.Patterns[pattern].GetNumRows() - 1 - curRow; pushRow > 0; pushRow--)
					{
						patData[col + pushRow * sndFile.GetNumChannels()] = patData[col + (pushRow - 1) * sndFile.GetNumChannels()];
					}
					m.Clear();
				}

				PatternCursor::Columns firstCol = PatternCursor::lastColumn, lastCol = PatternCursor::firstColumn;

				// Note
				if(data[pos] != ' ' && (!doMixPaste || ((!doITStyleMix && origModCmd.note == NOTE_NONE) || 
					(doITStyleMix && origModCmd.note == NOTE_NONE && origModCmd.instr == 0 && origModCmd.volcmd == VOLCMD_NONE))))
				{
					firstCol = PatternCursor::noteColumn;
					m.note = NOTE_NONE;
					if(data[pos] == '=')
						m.note = NOTE_KEYOFF;
					else if(data[pos] == '^')
						m.note = NOTE_NOTECUT;
					else if(data[pos] == '~')
						m.note = NOTE_FADE;
					else if(data[pos] == 'P')
					{
						if(data[pos + 2] == 'S' || data[pos + 2] == 's')
							m.note = NOTE_PCS;
						else
							m.note = NOTE_PC;
					} else if (data[pos] != '.')
					{
						// Check note names
						for(uint8 i = 0; i < 12; i++)
						{
							if(data[pos] == NoteNamesSharp[i][0] && data[pos + 1] == NoteNamesSharp[i][1])
							{
								m.note = ModCommand::NOTE(i + NOTE_MIN);
								break;
							}
						}
						if(m.note != NOTE_NONE)
						{
							// Check octave
							m.note += (data[pos + 2] - '0') * 12;
							if(!m.IsNote())
							{
								// Invalid octave
								m.note = NOTE_NONE;
							}
						}
					}
				}

				// Instrument
				if(data[pos + 3] > ' ' && (!doMixPaste || ( (!doITStyleMix && origModCmd.instr == 0) || 
					(doITStyleMix && origModCmd.note == NOTE_NONE && origModCmd.instr == 0 && origModCmd.volcmd == VOLCMD_NONE) ) ))
				{
					firstCol = std::min(firstCol, PatternCursor::instrColumn);
					lastCol = std::max(lastCol, PatternCursor::instrColumn);
					if(data[pos + 3] >= '0' && data[pos + 3] <= ('0' + (MAX_INSTRUMENTS / 10)))
					{
						m.instr = (data[pos + 3] - '0') * 10 + (data[pos + 4] - '0');
					} else m.instr = 0;
				}

				// Volume
				if(data[pos + 5] > ' ' && (!doMixPaste || ((!doITStyleMix && origModCmd.volcmd == VOLCMD_NONE) || 
					(doITStyleMix && origModCmd.note == NOTE_NONE && origModCmd.instr == 0 && origModCmd.volcmd == VOLCMD_NONE))))
				{
					firstCol = std::min(firstCol, PatternCursor::volumeColumn);
					lastCol = std::max(lastCol, PatternCursor::volumeColumn);
					if(data[pos + 5] != '.')
					{
						if(m.IsPcNote())
						{
							m.SetValueVolCol(ConvertStrTo<uint16>(data.substr(pos + 5, 3)));
						} else
						{
							m.volcmd = VOLCMD_NONE;
							for(int i = VOLCMD_NONE + 1; i < MAX_VOLCMDS; i++)
							{
								const char cmd = sourceSpecs.GetVolEffectLetter(static_cast<VolumeCommand>(i));
								if(data[pos + 5] == cmd && cmd != '?')
								{
									m.volcmd = static_cast<VolumeCommand>(i);
									break;
								}
							}
							m.vol = (data[pos + 6] - '0') * 10 + (data[pos + 7] - '0');
						}
					} else
					{
						m.volcmd = VOLCMD_NONE;
						m.vol = 0;
					}
				}

				// Effect
				if(m.IsPcNote())
				{
					if(data[pos + 8] != '.' && data[pos + 8] > ' ')
					{
						firstCol = std::min(firstCol, PatternCursor::paramColumn);
						lastCol = std::max(lastCol, PatternCursor::paramColumn);
						m.SetValueEffectCol(ConvertStrTo<uint16>(data.substr(pos + 8, 3)));
					} else if(!origModCmd.IsPcNote())
					{
						// No value provided in clipboard
						if((m.command == CMD_MIDI || m.command == CMD_SMOOTHMIDI) && m.param < 128)
							m.SetValueEffectCol(static_cast<uint16>(Util::muldivr(m.param, ModCommand::maxColumnValue, 127)));
						else
							m.SetValueEffectCol(0);
					}
				} else
				{
					if(data[pos + 8] > ' ' && (!doMixPaste || ((!doITStyleMix && origModCmd.command == CMD_NONE) || 
						(doITStyleMix && origModCmd.command == CMD_NONE && origModCmd.param == 0))))
					{
						firstCol = std::min(firstCol, PatternCursor::effectColumn);
						lastCol = std::max(lastCol, PatternCursor::effectColumn);
						m.command = CMD_NONE;
						if(data[pos + 8] != '.')
						{
							for(int i = CMD_NONE + 1; i < MAX_EFFECTS; i++)
							{
								const char cmd = sourceSpecs.GetEffectLetter(static_cast<EffectCommand>(i));
								if(data[pos + 8] == cmd && cmd != '?')
								{
									m.command = static_cast<EffectCommand>(i);
									break;
								}
							}
						}
					}

					// Effect value
					if(data[pos + 9] > ' ' && (!doMixPaste || ((!doITStyleMix && (origModCmd.command == CMD_NONE || origModCmd.param == 0)) || 
						(doITStyleMix && origModCmd.command == CMD_NONE && origModCmd.param == 0))))
					{
						firstCol = std::min(firstCol, PatternCursor::paramColumn);
						lastCol = std::max(lastCol, PatternCursor::paramColumn);
						m.param = 0;
						if(data[pos + 9] != '.')
						{
							for(uint8 i = 0; i < 16; i++)
							{
								if(data[pos + 9] == szHexChar[i]) m.param |= (i << 4);
								if(data[pos + 10] == szHexChar[i]) m.param |= i;
							}
						}
					}
					
					// Speed / tempo command conversion
					if (sndFile.GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM))
					{
						switch(m.command)
						{
						case CMD_SPEED:
						case CMD_TEMPO:
							if(!clipboardHasS3MCommands)
							{
								if(m.param < 32)
									m.command = CMD_SPEED;
								else
									m.command = CMD_TEMPO;
							} else
							{
								if(m.command == CMD_SPEED && m.param >= 32)
									m.param = CMD_TEMPO;
								else if(m.command == CMD_TEMPO && m.param < 32)
									m.param = CMD_SPEED;
							}
							break;
						}
					} else
					{
						switch(m.command)
						{
						case CMD_SPEED:
						case CMD_TEMPO:
							if(!clipboardHasS3MCommands)
							{
								if(m.param  < 32)
									m.command = CMD_SPEED;
								else
									m.command = CMD_TEMPO;
							}
							break;
						}
					}
				}

				// Convert some commands, if necessary. With mix paste convert only
				// if the original modcommand was empty as otherwise the unchanged parts
				// of the old modcommand would falsely be interpreted being of type
				// origFormat and ConvertCommand could change them.
				if(pasteFormat != sndFile.GetType() && (!doMixPaste || origModCmd.IsEmpty()))
					m.Convert(pasteFormat, sndFile.GetType(), sndFile);

				// Sanitize PC events
				if(m.IsPcNote())
				{
					m.SetValueEffectCol(std::min(m.GetValueEffectCol(), static_cast<decltype(m.GetValueEffectCol())>(ModCommand::maxColumnValue)));
					m.SetValueVolCol(std::min(m.GetValueVolCol(), static_cast<decltype(m.GetValueEffectCol())>(ModCommand::maxColumnValue)));
				}

				// Adjust pattern selection
				if(col == startChan) startPoint.SetColumn(startChan, firstCol);
				if(endPoint.CompareColumn(PatternCursor(0, col, lastCol)) < 0) endPoint.SetColumn(col, lastCol);
				if(curRow > endPoint.GetRow()) endPoint.SetRow(curRow);
				pasteRect = PatternRect(startPoint, endPoint);
			}

			pos += 11;
			col++;
		}
		// Next row
		patData += sndFile.GetNumChannels();
		curRow++;
		pos = eol;
	}

	return success;
}


// Copy one of the internal clipboards to the system clipboard.
bool PatternClipboard::SelectClipboard(clipindex_t which)
{
	instance.m_activeClipboard = which;
	return ToSystemClipboard(instance.m_clipboards[instance.m_activeClipboard]);
}


// Switch to the next internal clipboard.
bool PatternClipboard::CycleForward()
{
	instance.m_activeClipboard++;
	if(instance.m_activeClipboard >= instance.m_clipboards.size())
		instance.m_activeClipboard = 0;

	return SelectClipboard(instance.m_activeClipboard);
}


// Switch to the previous internal clipboard.
bool PatternClipboard::CycleBackward()
{
	if(instance.m_activeClipboard == 0)
		instance.m_activeClipboard = instance.m_clipboards.size() - 1;
	else
		instance.m_activeClipboard--;

	return SelectClipboard(instance.m_activeClipboard);
}


// Set the maximum number of internal clipboards.
void PatternClipboard::SetClipboardSize(clipindex_t maxEntries)
{
	instance.m_clipboards.resize(maxEntries, {"", _T("unused")});
	LimitMax(instance.m_activeClipboard, maxEntries - 1);
}


// Check whether patterns can be pasted from clipboard
bool PatternClipboard::CanPaste()
{
	return !!IsClipboardFormatAvailable(CF_TEXT);
}



// System-specific clipboard functions
bool PatternClipboard::ToSystemClipboard(const std::string_view &data)
{
	Clipboard clipboard(CF_TEXT, data.size() + 1);
	if(auto dst = clipboard.As<char>())
	{
		std::copy(data.begin(), data.end(), dst);
		dst[data.size()] = '\0';
		return true;
	}
	return false;
}


// System-specific clipboard functions
bool PatternClipboard::FromSystemClipboard(std::string &data)
{
	Clipboard clipboard(CF_TEXT);
	if(auto cbdata = clipboard.Get(); cbdata.data())
	{
		if(cbdata.size() > 0)
			data.assign(mpt::byte_cast<char *>(cbdata.data()), cbdata.size() - 1);
		return !data.empty();
	}
	return false;
}


BEGIN_MESSAGE_MAP(PatternClipboardDialog, ResizableDialog)
	ON_EN_UPDATE(IDC_EDIT1,     &PatternClipboardDialog::OnNumClipboardsChanged)
	ON_LBN_SELCHANGE(IDC_LIST1, &PatternClipboardDialog::OnSelectClipboard)
	ON_LBN_DBLCLK(IDC_LIST1,    &PatternClipboardDialog::OnEditName)
END_MESSAGE_MAP()

PatternClipboardDialog PatternClipboardDialog::instance;

void PatternClipboardDialog::DoDataExchange(CDataExchange *pDX)
{
	DDX_Control(pDX, IDC_SPIN1, m_numClipboardsSpin);
	DDX_Control(pDX, IDC_LIST1, m_clipList);
}


PatternClipboardDialog::PatternClipboardDialog() : m_editNameBox(*this)
{
}


void PatternClipboardDialog::Show()
{
	instance.m_isLocked = true;
	if(!instance.m_isCreated)
	{
		instance.Create(IDD_CLIPBOARD, CMainFrame::GetMainFrame());
		instance.m_numClipboardsSpin.SetRange(0, int16_max);
	}
	instance.SetDlgItemInt(IDC_EDIT1, mpt::saturate_cast<UINT>(PatternClipboard::GetClipboardSize()), FALSE);
	instance.m_isLocked = false;
	instance.m_isCreated = true;
	instance.UpdateList();
	
	instance.SetWindowPos(nullptr, instance.m_posX, instance.m_posY, 0, 0, SWP_SHOWWINDOW | SWP_NOOWNERZORDER | SWP_NOSIZE | SWP_NOZORDER | (instance.m_posX == -1 ? SWP_NOMOVE : 0));
}


void PatternClipboardDialog::OnNumClipboardsChanged()
{
	if(m_isLocked)
	{
		return;
	}
	OnEndEdit();
	PatternClipboard::SetClipboardSize(GetDlgItemInt(IDC_EDIT1, nullptr, FALSE));
	UpdateList();
}


void PatternClipboardDialog::UpdateList()
{
	if(instance.m_isLocked)
	{
		return;
	}
	instance.m_clipList.ResetContent();
	PatternClipboard::clipindex_t i = 0;
	for(const auto &clip : PatternClipboard::instance.m_clipboards)
	{
		const int item = instance.m_clipList.AddString(clip.description);
		instance.m_clipList.SetItemDataPtr(item, reinterpret_cast<void *>(i));
		if(PatternClipboard::instance.m_activeClipboard == i)
		{
			instance.m_clipList.SetCurSel(item);
		}
		i++;
	}
}


void PatternClipboardDialog::OnSelectClipboard()
{
	if(m_isLocked)
	{
		return;
	}
	PatternClipboard::clipindex_t item = reinterpret_cast<PatternClipboard::clipindex_t>(m_clipList.GetItemDataPtr(m_clipList.GetCurSel()));
	PatternClipboard::SelectClipboard(item);
	OnEndEdit();
}


void PatternClipboardDialog::OnOK()
{
	const CWnd *focus = GetFocus();
	if(focus == &m_editNameBox)
	{
		// User pressed enter in clipboard name edit box => cancel editing
		OnEndEdit();
	} else if(focus == &m_clipList)
	{
		// User pressed enter in the clipboard name list => start editing
		OnEditName();
	} else
	{
		ResizableDialog::OnOK();
	}
}


void PatternClipboardDialog::OnCancel()
{
	if(GetFocus() == &m_editNameBox)
	{
		// User pressed enter in clipboard name edit box => just cancel editing
		m_editNameBox.DestroyWindow();
		return;
	}

	OnEndEdit(false);

	m_isCreated = false;
	m_isLocked = true;

	RECT rect;
	GetWindowRect(&rect);
	m_posX = rect.left;
	m_posY = rect.top;

	DestroyWindow();
}


void PatternClipboardDialog::OnEditName()
{
	OnEndEdit();

	const int sel = m_clipList.GetCurSel();
	if(sel == LB_ERR)
	{
		return;
	}

	CRect rect;
	m_clipList.GetItemRect(sel, rect);
	rect.InflateRect(0, 2, 0, 2);

	// Create the edit control
	m_editNameBox.Create(WS_VISIBLE | WS_CHILD | WS_BORDER | ES_LEFT | ES_AUTOHSCROLL, rect, &m_clipList, 1);
	m_editNameBox.SetFont(m_clipList.GetFont());
	m_editNameBox.SetWindowText(PatternClipboard::instance.m_clipboards[sel].description);
	m_editNameBox.SetSel(0, -1, TRUE);
	m_editNameBox.SetFocus();
	SetWindowLongPtr(m_editNameBox.m_hWnd, GWLP_USERDATA, (LONG_PTR)m_clipList.GetItemDataPtr(sel));
}


void PatternClipboardDialog::OnEndEdit(bool apply)
{
	if(m_editNameBox.GetSafeHwnd() == NULL)
	{
		return;
	}

	if(apply)
	{
		size_t sel = GetWindowLongPtr(m_editNameBox.m_hWnd, GWLP_USERDATA);
		if(sel >= PatternClipboard::instance.m_clipboards.size())
		{
			// What happened?
			return;
		}

		CString newName;
		m_editNameBox.GetWindowText(newName);

		PatternClipboard::instance.m_clipboards[sel].description = newName;
	}

	SetWindowLongPtr(m_editNameBox.m_hWnd, GWLP_USERDATA, LONG_PTR(-1));
	m_editNameBox.DestroyWindow();

	UpdateList();
}


BEGIN_MESSAGE_MAP(PatternClipboardDialog::CInlineEdit, CEdit)
	ON_WM_KILLFOCUS()
END_MESSAGE_MAP()


PatternClipboardDialog::CInlineEdit::CInlineEdit(PatternClipboardDialog &dlg) : parent(dlg)
{
}


void PatternClipboardDialog::CInlineEdit::OnKillFocus(CWnd *newWnd)
{
	parent.OnEndEdit(true);
	CEdit::OnKillFocus(newWnd);
}


OPENMPT_NAMESPACE_END