/*
 * Undo.cpp
 * --------
 * Purpose: Editor undo buffer functionality.
 * Notes  : (currently none)
 * Authors: Olivier Lapicque
 *          OpenMPT Devs
 * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
 */


#include "stdafx.h"
#include "Moddoc.h"
#include "Mainfrm.h"
#include "Undo.h"
#include "../common/mptStringBuffer.h"
#include "../tracklib/SampleEdit.h"
#include "../soundlib/modsmp_ctrl.h"


OPENMPT_NAMESPACE_BEGIN


/////////////////////////////////////////////////////////////////////////////////////////
// Pattern Undo Functions


// Remove all undo steps.
void CPatternUndo::ClearUndo()
{
	UndoBuffer.clear();
	RedoBuffer.clear();
}


// Create undo point.
//   Parameter list:
//   - pattern: Pattern of which an undo step should be created from.
//   - firstChn: first channel, 0-based.
//   - firstRow: first row, 0-based.
//   - numChns: width
//   - numRows: height
//   - description: Short description text of action for undo menu.
//   - linkToPrevious: Don't create a separate undo step, but link this to the previous undo event. Use this for commands that modify several patterns at once.
//   - storeChannelInfo: Also store current channel header information (pan / volume / etc. settings) and number of channels in this undo point.
bool CPatternUndo::PrepareUndo(PATTERNINDEX pattern, CHANNELINDEX firstChn, ROWINDEX firstRow, CHANNELINDEX numChns, ROWINDEX numRows, const char *description, bool linkToPrevious, bool storeChannelInfo)
{
	if(PrepareBuffer(UndoBuffer, pattern, firstChn, firstRow, numChns, numRows, description, linkToPrevious, storeChannelInfo))
	{
		RedoBuffer.clear();
		return true;
	}
	return false;
}


bool CPatternUndo::PrepareChannelUndo(CHANNELINDEX firstChn, CHANNELINDEX numChns, const char *description)
{
	return PrepareUndo(PATTERNINDEX_INVALID, firstChn, 0, numChns, 0, description, false, true);
}


bool CPatternUndo::PrepareBuffer(undobuf_t &buffer, PATTERNINDEX pattern, CHANNELINDEX firstChn, ROWINDEX firstRow, CHANNELINDEX numChns, ROWINDEX numRows, const char *description, bool linkToPrevious, bool storeChannelInfo) const
{
	const CSoundFile &sndFile = modDoc.GetSoundFile();
	const bool onlyChannelInfo = storeChannelInfo && numRows < 1;

	if(storeChannelInfo && pattern != PATTERNINDEX_INVALID && firstChn == 0 && numChns != sndFile.GetNumChannels())
	{
		numChns = sndFile.GetNumChannels();
	}

	ROWINDEX patRows = 0;
	if(sndFile.Patterns.IsValidPat(pattern))
	{
		patRows = sndFile.Patterns[pattern].GetNumRows();
		if((firstRow >= patRows) || (firstChn >= sndFile.GetNumChannels()))
			return false;
		if(numChns < 1 || numRows < 1)
			return false;
		if(firstRow + numRows >= patRows)
			numRows = patRows - firstRow;
		if(firstChn + numChns >= sndFile.GetNumChannels())
			numChns = sndFile.GetNumChannels() - firstChn;
	} else if(!onlyChannelInfo)
	{
		return false;
	}

	// Remove an undo step if there are too many.
	if(buffer.size() >= MAX_UNDO_LEVEL)
	{
		buffer.erase(buffer.begin(), buffer.begin() + (buffer.size() - MAX_UNDO_LEVEL + 1));
	}

	UndoInfo undo;
	undo.pattern = pattern;
	undo.numPatternRows = patRows;
	undo.firstChannel = firstChn;
	undo.firstRow = firstRow;
	undo.numChannels = numChns;
	undo.numRows = numRows;
	undo.linkToPrevious = linkToPrevious;
	undo.description = description;

	if(!onlyChannelInfo)
	{
		try
		{
			undo.content.resize(numRows * numChns);
		} catch(mpt::out_of_memory e)
		{
			mpt::delete_out_of_memory(e);
			return false;
		}
		const ModCommand *pPattern = sndFile.Patterns[pattern].GetpModCommand(firstRow, firstChn);
		auto pUndoData = undo.content.begin();
		for(ROWINDEX iy = 0; iy < numRows; iy++)
		{
			std::copy(pPattern, pPattern + numChns, pUndoData);
			pUndoData += numChns;
			pPattern += sndFile.GetNumChannels();
		}
	}

	if(storeChannelInfo)
	{
		undo.channelInfo.assign(std::begin(sndFile.ChnSettings) + firstChn, std::begin(sndFile.ChnSettings) + firstChn + numChns);
	}

	buffer.push_back(std::move(undo));

	if(!linkToPrevious)
		modDoc.UpdateAllViews(nullptr, UpdateHint().Undo());
	return true;
}


// Restore an undo point. Returns which pattern has been modified.
PATTERNINDEX CPatternUndo::Undo()
{
	return Undo(UndoBuffer, RedoBuffer, false);
}


// Restore an undo point. Returns which pattern has been modified.
PATTERNINDEX CPatternUndo::Redo()
{
	return Undo(RedoBuffer, UndoBuffer, false);
}


// Restore an undo point. Returns which pattern has been modified.
// linkedFromPrevious is true if a connected undo event is going to be deleted (can only be called internally).
PATTERNINDEX CPatternUndo::Undo(undobuf_t &fromBuf, undobuf_t &toBuf, bool linkedFromPrevious)
{
	CSoundFile &sndFile = modDoc.GetSoundFile();

	bool linkToPrevious = false;

	if(fromBuf.empty())
		return PATTERNINDEX_INVALID;

	// Select most recent undo slot
	const UndoInfo &undo = fromBuf.back();
	const bool onlyChannelSettings = undo.OnlyChannelSettings();
	const bool deletePattern = (undo.numPatternRows == DELETE_PATTERN) && !onlyChannelSettings;

	// Add this action to redo buffer if the pattern exists; otherwise add a special deletion redo step later
	const bool patternExists = sndFile.Patterns.IsValidPat(undo.pattern);
	if(patternExists || onlyChannelSettings)
		PrepareBuffer(toBuf, undo.pattern, undo.firstChannel, undo.firstRow, undo.numChannels, undo.numRows, undo.description, linkedFromPrevious, !undo.channelInfo.empty());

	const bool modifyChannels = !undo.channelInfo.empty();
	const CHANNELINDEX updateChannel = (undo.numChannels == 1) ? undo.firstChannel : CHANNELINDEX_INVALID;
	if(modifyChannels)
	{
		const bool modifyChannelCount =
		    (undo.pattern != PATTERNINDEX_INVALID && undo.channelInfo.size() != sndFile.GetNumChannels())
		    || (undo.pattern == PATTERNINDEX_INVALID && (undo.firstChannel + undo.channelInfo.size()) > sndFile.GetNumChannels());
		if(modifyChannelCount)
		{
			// Add or remove channels
			std::vector<CHANNELINDEX> channels(undo.channelInfo.size(), CHANNELINDEX_INVALID);
			const CHANNELINDEX copyCount = std::min(sndFile.GetNumChannels(), static_cast<CHANNELINDEX>(undo.channelInfo.size()));
			std::iota(channels.begin(), channels.begin() + copyCount, CHANNELINDEX(0));
			modDoc.ReArrangeChannels(channels, false);
		}
		if(undo.firstChannel + undo.channelInfo.size() <= sndFile.GetNumChannels())
		{
			std::move(undo.channelInfo.cbegin(), undo.channelInfo.cend(), std::begin(sndFile.ChnSettings) + undo.firstChannel);
		}

		// Channel mute status might have changed...
		for(CHANNELINDEX i = undo.firstChannel; i < sndFile.GetNumChannels(); i++)
		{
			modDoc.UpdateChannelMuteStatus(i);
		}
	}

	PATTERNINDEX pat = undo.pattern;
	if(deletePattern)
	{
		sndFile.Patterns.Remove(pat);
	} else if(undo.firstChannel + undo.numChannels <= sndFile.GetNumChannels() && !onlyChannelSettings)
	{
		if(!patternExists)
		{
			if(!sndFile.Patterns.Insert(pat, undo.numPatternRows))
			{
				fromBuf.pop_back();
				return PATTERNINDEX_INVALID;
			}
		} else if(sndFile.Patterns[pat].GetNumRows() != undo.numPatternRows)
		{
			sndFile.Patterns[pat].Resize(undo.numPatternRows);
		}

		linkToPrevious = undo.linkToPrevious;
		auto pUndoData = undo.content.cbegin();
		CPattern &pattern = sndFile.Patterns[pat];
		ModCommand *m = pattern.GetpModCommand(undo.firstRow, undo.firstChannel);
		const ROWINDEX numRows = std::min(undo.numRows, pattern.GetNumRows());
		for(ROWINDEX iy = 0; iy < numRows; iy++)
		{
			std::move(pUndoData, pUndoData + undo.numChannels, m);
			m += sndFile.GetNumChannels();
			pUndoData += undo.numChannels;
		}
	}

	if(!patternExists && !onlyChannelSettings)
	{
		// Redo a deletion
		auto &redo = fromBuf.back();
		redo.content.clear();
		redo.numPatternRows = DELETE_PATTERN;
		toBuf.push_back(std::move(redo));
	}
	fromBuf.pop_back();

	if(patternExists != sndFile.Patterns.IsValidPat(pat))
	{
		modDoc.UpdateAllViews(nullptr, PatternHint(pat).Names().Undo());
		modDoc.UpdateAllViews(nullptr, SequenceHint().Data());  // Pattern color will change in sequence
	} else
	{
		modDoc.UpdateAllViews(nullptr, UpdateHint().Undo());
	}
	if(modifyChannels)
		modDoc.UpdateAllViews(nullptr, GeneralHint(updateChannel).Channels());
	modDoc.SetModified();

	if(linkToPrevious)
	{
		pat = Undo(fromBuf, toBuf, true);
	}

	return pat;
}


// Public helper function to remove the most recent undo point.
void CPatternUndo::RemoveLastUndoStep()
{
	if(UndoBuffer.empty())
		return;

	UndoBuffer.pop_back();
	modDoc.UpdateAllViews(nullptr, UpdateHint().Undo());
}


CString CPatternUndo::GetName(const undobuf_t &buffer)
{
	if(buffer.empty())
		return CString();
	
	const UndoInfo &info = buffer.back();
	CString desc = mpt::ToCString(mpt::Charset::Locale, info.description);
	if(info.linkToPrevious)
		desc += _T(" (Multiple Patterns)");
	else if(info.OnlyChannelSettings() && info.numChannels > 1)
		desc += _T(" (Multiple Channels)");
	else if(info.OnlyChannelSettings())
		desc += MPT_CFORMAT(" (Channel {})")(info.firstChannel + 1);
	else
		desc += MPT_CFORMAT(" (Pat {} Row {} Chn {})")(info.pattern, info.firstRow, info.firstChannel + 1);
	return desc;
}


void CPatternUndo::RearrangePatterns(undobuf_t &buffer, const std::vector<PATTERNINDEX> &newIndex)
{
	for(auto &step : buffer)
	{
		if(step.pattern < newIndex.size())
			step.pattern = newIndex[step.pattern];
	}
}


void CPatternUndo::RearrangePatterns(const std::vector<PATTERNINDEX> &newIndex)
{
	RearrangePatterns(UndoBuffer, newIndex);
	RearrangePatterns(RedoBuffer, newIndex);
}


/////////////////////////////////////////////////////////////////////////////////////////
// Sample Undo Functions


// Remove all undo steps for all samples.
void CSampleUndo::ClearUndo()
{
	for(SAMPLEINDEX smp = 1; smp <= MAX_SAMPLES; smp++)
	{
		ClearUndo(UndoBuffer, smp);
		ClearUndo(RedoBuffer, smp);
	}
	UndoBuffer.clear();
	RedoBuffer.clear();
}


// Remove all undo steps of a given sample.
void CSampleUndo::ClearUndo(undobuf_t &buffer, const SAMPLEINDEX smp)
{
	if(!SampleBufferExists(buffer, smp)) return;

	while(!buffer[smp - 1].empty())
	{
		DeleteStep(buffer, smp, 0);
	}
}


// Create undo point for given sample.
// The main program has to tell what kind of changes are going to be made to the sample.
// That way, a lot of RAM can be saved, because some actions don't even require an undo sample buffer.
bool CSampleUndo::PrepareUndo(const SAMPLEINDEX smp, sampleUndoTypes changeType, const char *description, SmpLength changeStart, SmpLength changeEnd)
{
	if(PrepareBuffer(UndoBuffer, smp, changeType, description, changeStart, changeEnd))
	{
		ClearUndo(RedoBuffer, smp);
		return true;
	}
	return false;
}


bool CSampleUndo::PrepareBuffer(undobuf_t &buffer, const SAMPLEINDEX smp, sampleUndoTypes changeType, const char *description, SmpLength changeStart, SmpLength changeEnd)
{
	if(smp == 0 || smp >= MAX_SAMPLES) return false;
	if(!TrackerSettings::Instance().m_SampleUndoBufferSize.Get().GetSizeInBytes())
	{
		// Undo/Redo is disabled
		return false;
	}
	if(smp > buffer.size())
	{
		buffer.resize(smp);
	}

	// Remove an undo step if there are too many.
	while(buffer[smp - 1].size() >= MAX_UNDO_LEVEL)
	{
		DeleteStep(buffer, smp, 0);
	}
	
	// Create new undo slot
	UndoInfo undo;

	const CSoundFile &sndFile = modDoc.GetSoundFile();
	const ModSample &oldSample = sndFile.GetSample(smp);

	// Save old sample header
	undo.OldSample = oldSample;
	undo.oldName = sndFile.m_szNames[smp];
	undo.changeType = changeType;
	undo.description = description;

	if(changeType == sundo_replace)
	{
		// ensure that size information is correct here.
		changeStart = 0;
		changeEnd = oldSample.nLength;
	} else if(changeType == sundo_none)
	{
		// we do nothing...
		changeStart = changeEnd = 0;
	}

	if(changeStart > oldSample.nLength || changeStart > changeEnd)
	{
		// Something is surely screwed up.
		MPT_ASSERT(false);
		return false;
	}

	// Restrict amount of memory that's being used
	RestrictBufferSize();

	undo.changeStart = changeStart;
	undo.changeEnd = changeEnd;
	undo.samplePtr = nullptr;

	switch(changeType)
	{
	case sundo_none:	// we are done, no sample changes here.
	case sundo_invert:	// no action necessary, since those effects can be applied again to be undone.
	case sundo_reverse:	// ditto
	case sundo_unsign:	// ditto
	case sundo_insert:	// no action necessary, we already have stored the variables that are necessary.
		break;

	case sundo_update:
	case sundo_delete:
	case sundo_replace:
		if(oldSample.HasSampleData())
		{
			const uint8 bytesPerSample = oldSample.GetBytesPerSample();
			const SmpLength changeLen = changeEnd - changeStart;

			undo.samplePtr = ModSample::AllocateSample(changeLen, bytesPerSample);
			if(undo.samplePtr == nullptr) return false;
			memcpy(undo.samplePtr, oldSample.sampleb() + changeStart * bytesPerSample, changeLen * bytesPerSample);

#ifdef MPT_ALL_LOGGING
			const size_t nSize = (GetBufferCapacity(UndoBuffer) + GetBufferCapacity(RedoBuffer) + changeLen * bytesPerSample) >> 10;
			MPT_LOG_GLOBAL(LogDebug, "Undo", MPT_UFORMAT("Sample undo/redo buffer size is now {}.{} MB")(nSize >> 10, (nSize & 1023) * 100 / 1024));
#endif

		}
		break;

	default:
		MPT_ASSERT(false); // whoops, what's this? someone forgot to implement it, some code is obviously missing here!
		return false;
	}

	buffer[smp - 1].push_back(std::move(undo));

	modDoc.UpdateAllViews(nullptr, UpdateHint().Undo());

	return true;
}


// Restore undo point for given sample
bool CSampleUndo::Undo(const SAMPLEINDEX smp)
{
	return Undo(UndoBuffer, RedoBuffer, smp);
}


// Restore redo point for given sample
bool CSampleUndo::Redo(const SAMPLEINDEX smp)
{
	return Undo(RedoBuffer, UndoBuffer, smp);
}


// Restore undo/redo point for given sample
bool CSampleUndo::Undo(undobuf_t &fromBuf, undobuf_t &toBuf, const SAMPLEINDEX smp)
{
	if(!SampleBufferExists(fromBuf, smp) || fromBuf[smp - 1].empty()) return false;

	CSoundFile &sndFile = modDoc.GetSoundFile();

	// Select most recent undo slot and temporarily remove it from the buffer so that it won't get deleted by possible buffer size restrictions in PrepareBuffer()
	UndoInfo undo = fromBuf[smp - 1].back();
	fromBuf[smp - 1].pop_back();

	// When turning an undo point into a redo point (and vice versa), some action types need to be adjusted.
	sampleUndoTypes redoType = undo.changeType;
	if(redoType == sundo_delete)
		redoType = sundo_insert;
	else if(redoType == sundo_insert)
		redoType = sundo_delete;
	PrepareBuffer(toBuf, smp, redoType, undo.description, undo.changeStart, undo.changeEnd);

	ModSample &sample = sndFile.GetSample(smp);
	std::byte *pCurrentSample = mpt::void_cast<std::byte*>(sample.samplev());
	int8 *pNewSample = nullptr;	// a new sample is possibly going to be allocated, depending on what's going to be undone.
	bool keepOnDisk = sample.uFlags[SMP_KEEPONDISK];
	bool replace = false;

	uint8 bytesPerSample = undo.OldSample.GetBytesPerSample();
	SmpLength changeLen = undo.changeEnd - undo.changeStart;

	switch(undo.changeType)
	{
	case sundo_none:
		break;

	case sundo_invert:
		// invert again
		SampleEdit::InvertSample(sample, undo.changeStart, undo.changeEnd, sndFile);
		break;

	case sundo_reverse:
		// reverse again
		SampleEdit::ReverseSample(sample, undo.changeStart, undo.changeEnd, sndFile);
		break;

	case sundo_unsign:
		// unsign again
		SampleEdit::UnsignSample(sample, undo.changeStart, undo.changeEnd, sndFile);
		break;

	case sundo_insert:
		// delete inserted data
		MPT_ASSERT(changeLen == sample.nLength - undo.OldSample.nLength);
		if(undo.OldSample.nLength > 0)
		{
			memcpy(pCurrentSample + undo.changeStart * bytesPerSample, pCurrentSample + undo.changeEnd * bytesPerSample, (sample.nLength - undo.changeEnd) * bytesPerSample);
			// also clean the sample end
			memset(pCurrentSample + undo.OldSample.nLength * bytesPerSample, 0, (sample.nLength - undo.OldSample.nLength) * bytesPerSample);
		} else
		{
			replace = true;
		}
		break;

	case sundo_update:
		// simply replace what has been updated.
		if(sample.nLength < undo.changeEnd) return false;
		memcpy(pCurrentSample + undo.changeStart * bytesPerSample, undo.samplePtr, changeLen * bytesPerSample);
		break;

	case sundo_delete:
		// insert deleted data
		pNewSample = static_cast<int8 *>(ModSample::AllocateSample(undo.OldSample.nLength, bytesPerSample));
		if(pNewSample == nullptr) return false;
		replace = true;
		memcpy(pNewSample, pCurrentSample, undo.changeStart * bytesPerSample);
		memcpy(pNewSample + undo.changeStart * bytesPerSample, undo.samplePtr, changeLen * bytesPerSample);
		memcpy(pNewSample + undo.changeEnd * bytesPerSample, pCurrentSample + undo.changeStart * bytesPerSample, (undo.OldSample.nLength - undo.changeEnd) * bytesPerSample);
		break;

	case sundo_replace:
		// simply exchange sample pointer
		pNewSample = static_cast<int8 *>(undo.samplePtr);
		undo.samplePtr = nullptr; // prevent sample from being deleted
		replace = true;
		break;

	default:
		MPT_ASSERT(false); // whoops, what's this? someone forgot to implement it, some code is obviously missing here!
		return false;
	}

	// Restore old sample header
	sample = undo.OldSample;
	sample.pData.pSample = mpt::void_cast<void*>(pCurrentSample); // select the "correct" old sample
	sndFile.m_szNames[smp] = undo.oldName;

	if(replace)
	{
		ctrlSmp::ReplaceSample(sample, pNewSample, undo.OldSample.nLength, sndFile);
	}
	sample.PrecomputeLoops(sndFile, true);

	if(undo.changeType != sundo_none)
	{
		sample.uFlags.set(SMP_MODIFIED);
	}
	if(!keepOnDisk)
	{
		// Never re-enable the keep on disk flag after it was disabled.
		// This can lead to quite some dangerous situations when replacing samples.
		sample.uFlags.reset(SMP_KEEPONDISK);
	}

	fromBuf[smp - 1].push_back(std::move(undo));
	DeleteStep(fromBuf, smp, fromBuf[smp - 1].size() - 1);

	modDoc.UpdateAllViews(nullptr, UpdateHint().Undo());
	modDoc.SetModified();

	return true;
}


// Delete a given undo / redo step of a sample.
void CSampleUndo::DeleteStep(undobuf_t &buffer, const SAMPLEINDEX smp, const size_t step)
{
	if(!SampleBufferExists(buffer, smp) || step >= buffer[smp - 1].size()) return;
	ModSample::FreeSample(buffer[smp - 1][step].samplePtr);
	buffer[smp - 1].erase(buffer[smp - 1].begin() + step);
}


// Public helper function to remove the most recent undo point.
void CSampleUndo::RemoveLastUndoStep(const SAMPLEINDEX smp)
{
	if(!CanUndo(smp))
		return;

	DeleteStep(UndoBuffer, smp, UndoBuffer[smp - 1].size() - 1);
	modDoc.UpdateAllViews(nullptr, UpdateHint().Undo());
}


// Restrict undo buffer size so it won't grow too large.
// This is done in FIFO style, equally distributed over all sample slots (very simple).
void CSampleUndo::RestrictBufferSize()
{
	size_t capacity = GetBufferCapacity(UndoBuffer) + GetBufferCapacity(RedoBuffer);
	while(capacity > TrackerSettings::Instance().m_SampleUndoBufferSize.Get().GetSizeInBytes())
	{
		RestrictBufferSize(UndoBuffer, capacity);
		RestrictBufferSize(RedoBuffer, capacity);
	}
}


void CSampleUndo::RestrictBufferSize(undobuf_t &buffer, size_t &capacity)
{
	for(SAMPLEINDEX smp = 1; smp <= buffer.size(); smp++)
	{
		if(capacity <= TrackerSettings::Instance().m_SampleUndoBufferSize.Get().GetSizeInBytes()) return;
		for(size_t i = 0; i < buffer[smp - 1].size(); i++)
		{
			if(buffer[smp - 1][i].samplePtr != nullptr)
			{
				capacity -= (buffer[smp - 1][i].changeEnd - buffer[smp - 1][i].changeStart) * buffer[smp - 1][i].OldSample.GetBytesPerSample();
				for(size_t j = 0; j <= i; j++)
				{
					DeleteStep(buffer, smp, 0);
				}
				// Try to evenly spread out the restriction, i.e. move on to other samples before deleting another step for this sample.
				break;
			}
		}
	}
}


// Update undo buffer when using rearrange sample functionality.
// newIndex contains one new index for each old index. newIndex[1] represents the first sample.
void CSampleUndo::RearrangeSamples(undobuf_t &buffer, const std::vector<SAMPLEINDEX> &newIndex)
{
	undobuf_t newBuf(modDoc.GetNumSamples());

	const SAMPLEINDEX newSize = static_cast<SAMPLEINDEX>(newIndex.size());
	const SAMPLEINDEX oldSize = static_cast<SAMPLEINDEX>(buffer.size());
	for(SAMPLEINDEX smp = 1; smp <= oldSize; smp++)
	{
		MPT_ASSERT(smp >= newSize || newIndex[smp] <= modDoc.GetNumSamples());
		if(smp < newSize && newIndex[smp] > 0 && newIndex[smp] <= modDoc.GetNumSamples())
		{
			newBuf[newIndex[smp] - 1] = buffer[smp - 1];
		} else
		{
			ClearUndo(smp);
		}
	}
#ifdef _DEBUG
	for(size_t i = 0; i < oldSize; i++)
	{
		if(i + 1 < newIndex.size() && newIndex[i + 1] != 0)
			MPT_ASSERT(newBuf[newIndex[i + 1] - 1].size() == buffer[i].size());
		else
			MPT_ASSERT(buffer[i].empty());
	}
#endif
	buffer = newBuf;
}


// Return total amount of bytes used by the sample undo buffer.
size_t CSampleUndo::GetBufferCapacity(const undobuf_t &buffer) const
{
	size_t sum = 0;
	for(auto &smp : buffer)
	{
		for(auto &step : smp)
		{
			if(step.samplePtr != nullptr)
			{
				sum += (step.changeEnd - step.changeStart) * step.OldSample.GetBytesPerSample();
			}
		}
	}
	return sum;
}


// Ensure that the undo buffer is big enough for a given sample number
bool CSampleUndo::SampleBufferExists(const undobuf_t &buffer, const SAMPLEINDEX smp) const
{
	if(smp == 0 || smp >= MAX_SAMPLES) return false;
	if(smp <= buffer.size()) return true;
	return false;
}

// Get name of next undo item
const char *CSampleUndo::GetUndoName(const SAMPLEINDEX smp) const
{
	if(!CanUndo(smp))
	{
		return "";
	}
	return UndoBuffer[smp - 1].back().description;
}


// Get name of next redo item
const char *CSampleUndo::GetRedoName(const SAMPLEINDEX smp) const
{
	if(!CanRedo(smp))
	{
		return "";
	}
	return RedoBuffer[smp - 1].back().description;
}


/////////////////////////////////////////////////////////////////////////////////////////
// Instrument Undo Functions


// Remove all undo steps for all instruments.
void CInstrumentUndo::ClearUndo()
{
	UndoBuffer.clear();
	RedoBuffer.clear();
}


// Remove all undo steps of a given instrument.
void CInstrumentUndo::ClearUndo(undobuf_t &buffer, const INSTRUMENTINDEX ins)
{
	if(!InstrumentBufferExists(buffer, ins)) return;
	buffer[ins - 1].clear();
}


// Create undo point for given Instrument.
// The main program has to tell what kind of changes are going to be made to the Instrument.
// That way, a lot of RAM can be saved, because some actions don't even require an undo Instrument buffer.
bool CInstrumentUndo::PrepareUndo(const INSTRUMENTINDEX ins, const char *description, EnvelopeType envType)
{
	if(PrepareBuffer(UndoBuffer, ins, description, envType))
	{
		ClearUndo(RedoBuffer, ins);
		return true;
	}
	return false;
}


bool CInstrumentUndo::PrepareBuffer(undobuf_t &buffer, const INSTRUMENTINDEX ins, const char *description, EnvelopeType envType)
{
	if(ins == 0 || ins >= MAX_INSTRUMENTS || modDoc.GetSoundFile().Instruments[ins] == nullptr) return false;
	if(ins > buffer.size())
	{
		buffer.resize(ins);
	}
	auto &insBuffer = buffer[ins - 1];

	// Remove undo steps if there are too many.
	if(insBuffer.size() >= MAX_UNDO_LEVEL)
	{
		insBuffer.erase(insBuffer.begin(), insBuffer.begin() + (insBuffer.size() - MAX_UNDO_LEVEL + 1));
	}
	
	// Create new undo slot
	UndoInfo undo;

	const CSoundFile &sndFile = modDoc.GetSoundFile();
	undo.description = description;
	undo.editedEnvelope = envType;
	if(envType < ENV_MAXTYPES)
	{
		undo.instr.GetEnvelope(envType) = sndFile.Instruments[ins]->GetEnvelope(envType);
	} else
	{
		undo.instr = *sndFile.Instruments[ins];
	}
	// cppcheck false-positive
	// cppcheck-suppress uninitStructMember
	insBuffer.push_back(std::move(undo));

	modDoc.UpdateAllViews(nullptr, UpdateHint().Undo());

	return true;
}


// Restore undo point for given Instrument
bool CInstrumentUndo::Undo(const INSTRUMENTINDEX ins)
{
	return Undo(UndoBuffer, RedoBuffer, ins);
}


// Restore redo point for given Instrument
bool CInstrumentUndo::Redo(const INSTRUMENTINDEX ins)
{
	return Undo(RedoBuffer, UndoBuffer, ins);
}


// Restore undo/redo point for given Instrument
bool CInstrumentUndo::Undo(undobuf_t &fromBuf, undobuf_t &toBuf, const INSTRUMENTINDEX ins)
{
	CSoundFile &sndFile = modDoc.GetSoundFile();
	if(sndFile.Instruments[ins] == nullptr || !InstrumentBufferExists(fromBuf, ins) || fromBuf[ins - 1].empty()) return false;

	// Select most recent undo slot
	const UndoInfo &undo = fromBuf[ins - 1].back();

	PrepareBuffer(toBuf, ins, undo.description, undo.editedEnvelope);

	// When turning an undo point into a redo point (and vice versa), some action types need to be adjusted.
	ModInstrument *instr = sndFile.Instruments[ins];
	if(undo.editedEnvelope < ENV_MAXTYPES)
	{
		instr->GetEnvelope(undo.editedEnvelope) = undo.instr.GetEnvelope(undo.editedEnvelope);
	} else
	{
		*instr = undo.instr;
	}

	DeleteStep(fromBuf, ins, fromBuf[ins - 1].size() - 1);

	modDoc.UpdateAllViews(nullptr, UpdateHint().Undo());
	modDoc.SetModified();

	return true;
}


// Delete a given undo / redo step of a Instrument.
void CInstrumentUndo::DeleteStep(undobuf_t &buffer, const INSTRUMENTINDEX ins, const size_t step)
{
	if(!InstrumentBufferExists(buffer, ins) || step >= buffer[ins - 1].size()) return;
	buffer[ins - 1].erase(buffer[ins - 1].begin() + step);
}


// Public helper function to remove the most recent undo point.
void CInstrumentUndo::RemoveLastUndoStep(const INSTRUMENTINDEX ins)
{
	if(!CanUndo(ins)) return;
	DeleteStep(UndoBuffer, ins, UndoBuffer[ins - 1].size() - 1);
}


// Update undo buffer when using rearrange instruments functionality.
// newIndex contains one new index for each old index. newIndex[1] represents the first instrument.
void CInstrumentUndo::RearrangeInstruments(undobuf_t &buffer, const std::vector<INSTRUMENTINDEX> &newIndex)
{
	undobuf_t newBuf(modDoc.GetNumInstruments());

	const INSTRUMENTINDEX newSize = static_cast<INSTRUMENTINDEX>(newIndex.size());
	const INSTRUMENTINDEX oldSize = static_cast<INSTRUMENTINDEX>(buffer.size());
	for(INSTRUMENTINDEX ins = 1; ins <= oldSize; ins++)
	{
		MPT_ASSERT(ins >= newSize || newIndex[ins] <= modDoc.GetNumInstruments());
		if(ins < newSize && newIndex[ins] > 0 && newIndex[ins] <= modDoc.GetNumInstruments())
		{
			newBuf[newIndex[ins] - 1] = buffer[ins - 1];
		} else
		{
			ClearUndo(ins);
		}
	}
#ifdef _DEBUG
	for(size_t i = 0; i < oldSize; i++)
	{
		if(i + 1 < newIndex.size() && newIndex[i + 1] != 0)
			MPT_ASSERT(newBuf[newIndex[i + 1] - 1].size() == buffer[i].size());
		else
			MPT_ASSERT(buffer[i].empty());
	}
#endif
	buffer = newBuf;
}


// Update undo buffer when using rearrange samples functionality.
// newIndex contains one new index for each old index. newIndex[1] represents the first sample.
void CInstrumentUndo::RearrangeSamples(undobuf_t &buffer, const INSTRUMENTINDEX ins, std::vector<SAMPLEINDEX> &newIndex)
{
	const CSoundFile &sndFile = modDoc.GetSoundFile();
	if(sndFile.Instruments[ins] == nullptr || !InstrumentBufferExists(buffer, ins) || buffer[ins - 1].empty()) return;

	for(auto &i : buffer[ins - 1]) if(i.editedEnvelope >= ENV_MAXTYPES)
	{
		for(auto &sample : i.instr.Keyboard)
		{
			if(sample < newIndex.size())
				sample = newIndex[sample];
			else
				sample = 0;
		}
	}
}


// Ensure that the undo buffer is big enough for a given Instrument number
bool CInstrumentUndo::InstrumentBufferExists(const undobuf_t &buffer, const INSTRUMENTINDEX ins) const
{
	if(ins == 0 || ins >= MAX_INSTRUMENTS) return false;
	if(ins <= buffer.size()) return true;
	return false;
}


// Get name of next undo item
const char *CInstrumentUndo::GetUndoName(const INSTRUMENTINDEX ins) const
{
	if(!CanUndo(ins))
	{
		return "";
	}
	return UndoBuffer[ins - 1].back().description;
}


// Get name of next redo item
const char *CInstrumentUndo::GetRedoName(const INSTRUMENTINDEX ins) const
{
	if(!CanRedo(ins))
	{
		return "";
	}
	return RedoBuffer[ins - 1].back().description;
}


OPENMPT_NAMESPACE_END