/*
 * MPTHacks.cpp
 * ------------
 * Purpose: Find out if MOD/XM/S3M/IT modules have MPT-specific hacks and fix them.
 * Notes  : This is not finished yet. Still need to handle:
 *          - Out-of-range sample pre-amp settings
 *          - Comments in XM files
 *          - Many auto-fix actions (so that the auto-fix mode can actually be used at some point!)
 *          Maybe there should be two options if hacks are found: Convert the song to MPTM or remove hacks.
 * Authors: OpenMPT Devs
 * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
 */


#include "stdafx.h"
#include "Moddoc.h"
#include "../soundlib/modsmp_ctrl.h"
#include "../soundlib/mod_specifications.h"


OPENMPT_NAMESPACE_BEGIN


// Find and fix envelopes where two nodes are on the same tick.
bool FindIncompatibleEnvelopes(InstrumentEnvelope &env, bool autofix)
{
	bool found = false;
	for(uint32 i = 1; i < env.size(); i++)
	{
		if(env[i].tick <= env[i - 1].tick)	// "<=" so we can fix envelopes "on the fly"
		{
			found = true;
			if(autofix)
			{
				env[i].tick = env[i - 1].tick + 1;
			}
		}
	}
	return found;
}


// Go through the module to find out if it contains any hacks introduced by (Open)MPT
bool CModDoc::HasMPTHacks(const bool autofix)
{
	const CModSpecifications *originalSpecs = &m_SndFile.GetModSpecifications();
	// retrieve original (not hacked) specs.
	MODTYPE modType = m_SndFile.GetBestSaveFormat();
	switch(modType)
	{
	case MOD_TYPE_MOD:
		originalSpecs = &ModSpecs::mod;
		break;
	case MOD_TYPE_XM:
		originalSpecs = &ModSpecs::xm;
		break;
	case MOD_TYPE_S3M:
		originalSpecs = &ModSpecs::s3m;
		break;
	case MOD_TYPE_IT:
		originalSpecs = &ModSpecs::it;
		break;
	}

	bool foundHacks = false, foundHere = false;
	ClearLog();

	// Check for plugins
#ifndef NO_PLUGINS
	foundHere = false;
	for(const auto &plug : m_SndFile.m_MixPlugins)
	{
		if(plug.IsValidPlugin())
		{
			foundHere = foundHacks = true;
			break;
		}
		// REQUIRES AUTOFIX
	}
	if(foundHere)
		AddToLog("Found plugins");
#endif // NO_PLUGINS

	// Check for invalid order items
	if(!originalSpecs->hasIgnoreIndex && mpt::contains(m_SndFile.Order(), m_SndFile.Order.GetIgnoreIndex()))
	{
		foundHacks = true;
		AddToLog("This format does not support separator (+++) patterns");

		if(autofix)
		{
			m_SndFile.Order().RemovePattern(m_SndFile.Order.GetIgnoreIndex());
		}
	}

	if(!originalSpecs->hasStopIndex && m_SndFile.Order().GetLengthFirstEmpty() != m_SndFile.Order().GetLengthTailTrimmed())
	{
		foundHacks = true;
		AddToLog("The pattern sequence should end after the first stop (---) index in this format.");

		if(autofix)
		{
			m_SndFile.Order().RemovePattern(m_SndFile.Order.GetInvalidPatIndex());
		}
	}

	// Global volume
	if(modType == MOD_TYPE_XM && m_SndFile.m_nDefaultGlobalVolume != MAX_GLOBAL_VOLUME)
	{
		foundHacks = true;
		AddToLog("XM format does not support default global volume");
		if(autofix)
		{
			GlobalVolumeToPattern();
		}
	}

	// Pattern count
	if(m_SndFile.Patterns.GetNumPatterns() > originalSpecs->patternsMax)
	{
		AddToLog(MPT_AFORMAT("Found too many patterns ({} allowed)")(originalSpecs->patternsMax));
		foundHacks = true;
		// REQUIRES (INTELLIGENT) AUTOFIX
	}

	// Check for too big/small patterns
	foundHere = false;
	for(auto &pat : m_SndFile.Patterns)
	{
		if(pat.IsValid())
		{
			const ROWINDEX patSize = pat.GetNumRows();
			if(patSize > originalSpecs->patternRowsMax)
			{
				foundHacks = foundHere = true;
				if(autofix)
				{
					// REQUIRES (INTELLIGENT) AUTOFIX
				} else
				{
					break;
				}
			} else if(patSize < originalSpecs->patternRowsMin)
			{
				foundHacks = foundHere = true;
				if(autofix)
				{
					pat.Resize(originalSpecs->patternRowsMin);
					pat.WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(patSize - 1).RetryNextRow());
				} else
				{
					break;
				}
			}
		}
	}
	if(foundHere)
	{
		AddToLog(MPT_AFORMAT("Found incompatible pattern lengths (must be between {} and {} rows)")(originalSpecs->patternRowsMin, originalSpecs->patternRowsMax));
	}

	// Check for invalid pattern commands
	foundHere = false;
	m_SndFile.Patterns.ForEachModCommand([originalSpecs, &foundHere, autofix, modType] (ModCommand &m)
	{
		// definitely not perfect yet. :)
		// Probably missing: Some extended effect parameters
		if(!originalSpecs->HasNote(m.note))
		{
			foundHere = true;
			if(autofix)
				m.note = NOTE_NONE;
		}

		if(!originalSpecs->HasCommand(m.command))
		{
			foundHere = true;
			if(autofix)
				m.command = CMD_NONE;
		}

		if(!originalSpecs->HasVolCommand(m.volcmd))
		{
			foundHere = true;
			if(autofix)
				m.volcmd = VOLCMD_NONE;
		}

		if(modType == MOD_TYPE_XM)		// ModPlug XM extensions
		{
			if(m.command == CMD_XFINEPORTAUPDOWN && m.param >= 0x30)
			{
				foundHere = true;
				if(autofix)
					m.command = CMD_NONE;
			}
		} else if(modType == MOD_TYPE_IT)		// ModPlug IT extensions
		{
			if((m.command == CMD_S3MCMDEX) && ((m.param & 0xF0) == 0x90) && (m.param != 0x91))
			{
				foundHere = true;
				if(autofix)
					m.command = CMD_NONE;
			}
		}
	});
	if(foundHere)
	{
		AddToLog("Found invalid pattern commands");
		foundHacks = true;
	}

	// Check for pattern names
	const PATTERNINDEX numNamedPatterns = m_SndFile.Patterns.GetNumNamedPatterns();
	if(numNamedPatterns > 0 && !originalSpecs->hasPatternNames)
	{
		AddToLog("Found pattern names");
		foundHacks = true;
		if(autofix)
		{
			for(PATTERNINDEX i = 0; i < numNamedPatterns; i++)
			{
				m_SndFile.Patterns[i].SetName("");
			}
		}
	}

	// Check for too many channels
	if(m_SndFile.GetNumChannels() > originalSpecs->channelsMax || m_SndFile.GetNumChannels() < originalSpecs->channelsMin)
	{
		AddToLog(MPT_AFORMAT("Found incompatible channel count (must be between {} and {} channels)")(originalSpecs->channelsMin, originalSpecs->channelsMax));
		foundHacks = true;
		if(autofix)
		{
			std::vector<bool> usedChannels;
			CheckUsedChannels(usedChannels);
			RemoveChannels(usedChannels);
			// REQUIRES (INTELLIGENT) AUTOFIX
		}
	}

	// Check for channel names
	foundHere = false;
	for(CHANNELINDEX i = 0; i < m_SndFile.GetNumChannels(); i++)
	{
		if(!m_SndFile.ChnSettings[i].szName.empty())
		{
			foundHere = foundHacks = true;
			if(autofix)
				m_SndFile.ChnSettings[i].szName = "";
			else
				break;
		}
	}
	if(foundHere)
		AddToLog("Found channel names");

	// Check for too many samples
	if(m_SndFile.GetNumSamples() > originalSpecs->samplesMax)
	{
		AddToLog(MPT_AFORMAT("Found too many samples ({} allowed)")(originalSpecs->samplesMax));
		foundHacks = true;
		// REQUIRES (INTELLIGENT) AUTOFIX
	}

	// Check for sample extensions
	foundHere = false;
	for(SAMPLEINDEX i = 1; i <= m_SndFile.GetNumSamples(); i++)
	{
		ModSample &smp = m_SndFile.GetSample(i);
		if(modType == MOD_TYPE_XM && smp.GetNumChannels() > 1)
		{
			foundHere = foundHacks = true;
			if(autofix)
			{
				ctrlSmp::ConvertToMono(smp, m_SndFile, ctrlSmp::mixChannels);
			} else
			{
				break;
			}
		}
	}
	if(foundHere)
		AddToLog("Stereo samples are not supported in the original XM format");

	// Check for too many instruments
	if(m_SndFile.GetNumInstruments() > originalSpecs->instrumentsMax)
	{
		AddToLog(MPT_AFORMAT("Found too many instruments ({} allowed)")(originalSpecs->instrumentsMax));
		foundHacks = true;
		// REQUIRES (INTELLIGENT) AUTOFIX
	}

	// Check for instrument extensions
	foundHere = false;
	bool foundEnvelopes = false;
	for(INSTRUMENTINDEX i = 1; i <= m_SndFile.GetNumInstruments(); i++)
	{
		ModInstrument *instr = m_SndFile.Instruments[i];
		if(instr == nullptr) continue;

		// Extended instrument attributes
		if(instr->filterMode != FilterMode::Unchanged || instr->nVolRampUp != 0 || instr->resampling != SRCMODE_DEFAULT ||
			instr->nCutSwing != 0 || instr->nResSwing != 0 || instr->nMixPlug != 0 || instr->pitchToTempoLock.GetRaw() != 0 ||
			instr->nDCT == DuplicateCheckType::Plugin ||
			instr->VolEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET ||
			instr->PanEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET ||
			instr->PitchEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET
		)
		{
			foundHere = foundHacks = true;
			if(autofix)
			{
				instr->filterMode = FilterMode::Unchanged;
				instr->nVolRampUp = 0;
				instr->resampling = SRCMODE_DEFAULT;
				instr->nCutSwing = 0;
				instr->nResSwing = 0;
				instr->nMixPlug = 0;
				instr->pitchToTempoLock.Set(0);
				if(instr->nDCT == DuplicateCheckType::Plugin) instr->nDCT = DuplicateCheckType::None;
				instr->VolEnv.nReleaseNode = instr->PanEnv.nReleaseNode = instr->PitchEnv.nReleaseNode = ENV_RELEASE_NODE_UNSET;
			}
		}
		// Incompatible envelope shape
		foundEnvelopes |= FindIncompatibleEnvelopes(instr->VolEnv, autofix);
		foundEnvelopes |= FindIncompatibleEnvelopes(instr->PanEnv, autofix);
		foundEnvelopes |= FindIncompatibleEnvelopes(instr->PitchEnv, autofix);
		foundHacks |= foundEnvelopes;
	}
	if(foundHere)
		AddToLog("Found MPT instrument extensions");
	if(foundEnvelopes)
		AddToLog("Two envelope points may not share the same tick.");

	// Check for too many orders
	if(m_SndFile.Order().GetLengthTailTrimmed() > originalSpecs->ordersMax)
	{
		AddToLog(MPT_AFORMAT("Found too many orders ({} allowed)")(originalSpecs->ordersMax));
		foundHacks = true;
		if(autofix)
		{
			// Can we be more intelligent here and maybe remove stop patterns and such?
			m_SndFile.Order().resize(originalSpecs->ordersMax);
		}
	}

	// Check for invalid default tempo
	if(m_SndFile.m_nDefaultTempo > originalSpecs->GetTempoMax() || m_SndFile.m_nDefaultTempo < originalSpecs->GetTempoMin())
	{
		AddToLog(MPT_AFORMAT("Found incompatible default tempo (must be between {} and {})")(originalSpecs->GetTempoMin().GetInt(), originalSpecs->GetTempoMax().GetInt()));
		foundHacks = true;
		if(autofix)
			m_SndFile.m_nDefaultTempo = Clamp(m_SndFile.m_nDefaultTempo, originalSpecs->GetTempoMin(), originalSpecs->GetTempoMax());
	}

	// Check for invalid default speed
	if(m_SndFile.m_nDefaultSpeed > originalSpecs->speedMax || m_SndFile.m_nDefaultSpeed < originalSpecs->speedMin)
	{
		AddToLog(MPT_AFORMAT("Found incompatible default speed (must be between {} and {})")(originalSpecs->speedMin, originalSpecs->speedMax));
		foundHacks = true;
		if(autofix)
			m_SndFile.m_nDefaultSpeed = Clamp(m_SndFile.m_nDefaultSpeed, originalSpecs->speedMin, originalSpecs->speedMax);
	}

	// Check for invalid rows per beat / measure values
	if(m_SndFile.m_nDefaultRowsPerBeat >= originalSpecs->patternRowsMax || m_SndFile.m_nDefaultRowsPerMeasure >= originalSpecs->patternRowsMax)
	{
		AddToLog("Found incompatible rows per beat / measure");
		foundHacks = true;
		if(autofix)
		{
			m_SndFile.m_nDefaultRowsPerBeat = Clamp(m_SndFile.m_nDefaultRowsPerBeat, 1u, (originalSpecs->patternRowsMax - 1));
			m_SndFile.m_nDefaultRowsPerMeasure = Clamp(m_SndFile.m_nDefaultRowsPerMeasure, m_SndFile.m_nDefaultRowsPerBeat, (originalSpecs->patternRowsMax - 1));
		}
	}

	// Find pattern-specific time signatures
	if(!originalSpecs->hasPatternSignatures)
	{
		foundHere = false;
		for(auto &pat : m_SndFile.Patterns)
		{
			if(pat.GetOverrideSignature())
			{
				if(!foundHere)
					AddToLog("Found pattern-specific time signatures");

				if(autofix)
					pat.RemoveSignature();

				foundHacks = foundHere = true;
				if(!autofix)
					break;
			}
		}
	}

	// Check for new tempo modes
	if(m_SndFile.m_nTempoMode != TempoMode::Classic)
	{
		AddToLog("Found incompatible tempo mode (only classic tempo mode allowed)");
		foundHacks = true;
		if(autofix)
			m_SndFile.m_nTempoMode = TempoMode::Classic;
	}

	// Check for extended filter range flag
	if(m_SndFile.m_SongFlags[SONG_EXFILTERRANGE])
	{
		AddToLog("Found extended filter range");
		foundHacks = true;
		if(autofix)
			m_SndFile.m_SongFlags.reset(SONG_EXFILTERRANGE);
	}

	// Player flags
	if((modType & (MOD_TYPE_XM|MOD_TYPE_IT)) && !m_SndFile.m_playBehaviour[MSF_COMPATIBLE_PLAY])
	{
		AddToLog("Compatible play is deactivated");
		foundHacks = true;
		if(autofix)
			m_SndFile.SetDefaultPlaybackBehaviour(modType);
	}

	// Check for restart position where it should not be
	for(SEQUENCEINDEX seq = 0; seq < m_SndFile.Order.GetNumSequences(); seq++)
	{
		if(m_SndFile.Order(seq).GetRestartPos() > 0 && !originalSpecs->hasRestartPos)
		{
			AddToLog("Found restart position");
			foundHacks = true;
			if(autofix)
			{
				m_SndFile.Order.RestartPosToPattern(seq);
			}
		}
	}

	if(!originalSpecs->hasArtistName && !m_SndFile.m_songArtist.empty() && !(modType & (MOD_TYPE_MOD | MOD_TYPE_S3M)))
	{
		AddToLog("Found artist name");
		foundHacks = true;
		if(autofix)
		{
			m_SndFile.m_songArtist.clear();
		}
	}

	if(m_SndFile.GetMixLevels() != MixLevels::Compatible && m_SndFile.GetMixLevels() != MixLevels::CompatibleFT2)
	{
		AddToLog("Found incorrect mix levels (only compatible mix levels allowed)");
		foundHacks = true;
		if(autofix)
			m_SndFile.SetMixLevels(modType == MOD_TYPE_XM ? MixLevels::CompatibleFT2 : MixLevels::Compatible);
	}

	// Check for extended MIDI macros
	if(modType == MOD_TYPE_IT)
	{
		for(const auto &macro : m_SndFile.m_MidiCfg)
		{
			for(const auto c : std::string_view{macro})
			{
				if(c == 's')
				{
					foundHacks = true;
					AddToLog("Found SysEx checksum variable in MIDI macro");
					break;
				}
			}
		}
	}

	if(autofix && foundHacks)
		SetModified();

	return foundHacks;
}


OPENMPT_NAMESPACE_END