/*
 * Load_sfx.cpp
 * ------------
 * Purpose: SFX / MMS (SoundFX / MultiMedia Sound) module loader
 * Notes  : Mostly based on the Soundtracker loader, some effect behavior is based on Flod's implementation.
 * Authors: Devin Acker
 *          OpenMPT Devs
 * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
 */

#include "stdafx.h"
#include "Loaders.h"
#include "Tables.h"

OPENMPT_NAMESPACE_BEGIN

#ifdef NEEDS_PRAGMA_PACK
#pragma pack(push, 1)
#endif

// File Header
struct PACKED SFXFileHeader
{
	uint8 numOrders;
	uint8 restartPos;
	uint8 orderList[128];
};

STATIC_ASSERT(sizeof(SFXFileHeader) == 130);

// Sample Header
struct PACKED SFXSampleHeader
{
	char   name[22];
	char   dummy[2];	// Supposedly sample length, but almost always incorrect
	uint8  finetune;
	uint8  volume;
	uint16 loopStart;
	uint16 loopLength;

	// Convert all multi-byte numeric values to current platform's endianness or vice versa.
	void ConvertEndianness()
	{
		SwapBytesBE(loopStart);
		SwapBytesBE(loopLength);
	}

	// Convert an MOD sample header to OpenMPT's internal sample header.
	void ConvertToMPT(ModSample &mptSmp, uint32 length) const
	{
		mptSmp.Initialize(MOD_TYPE_MOD);
		mptSmp.nLength = length;
		mptSmp.nFineTune = MOD2XMFineTune(finetune);
		mptSmp.nVolume = 4u * std::min(volume, uint8(64));

		SmpLength lStart = loopStart;
		SmpLength lLength = loopLength * 2u;

		if(mptSmp.nLength)
		{
			mptSmp.nLoopStart = lStart;
			mptSmp.nLoopEnd = lStart + lLength;

			if(mptSmp.nLoopStart >= mptSmp.nLength)
			{
				mptSmp.nLoopStart = mptSmp.nLength - 1;
			}
			if(mptSmp.nLoopEnd > mptSmp.nLength)
			{
				mptSmp.nLoopEnd = mptSmp.nLength;
			}
			if(mptSmp.nLoopStart > mptSmp.nLoopEnd || mptSmp.nLoopEnd < 4 || mptSmp.nLoopEnd - mptSmp.nLoopStart < 4)
			{
				mptSmp.nLoopStart = 0;
				mptSmp.nLoopEnd = 0;
			}

			if(mptSmp.nLoopEnd > mptSmp.nLoopStart)
			{
				mptSmp.uFlags.set(CHN_LOOP);
			}
		}
	}
};

STATIC_ASSERT(sizeof(SFXSampleHeader) == 30);

#ifdef NEEDS_PRAGMA_PACK
#pragma pack(pop)
#endif
static uint8 ClampSlideParam(uint8 value, uint8 lowNote, uint8 highNote)
//----------------------------------------------------------------------
{
	uint16 lowPeriod, highPeriod;

	if(lowNote  < highNote &&
	   lowNote  >= 36 + NOTE_MIN &&
	   highNote >= 36 + NOTE_MIN &&
	   lowNote  < CountOf(ProTrackerPeriodTable) + 36 + NOTE_MIN &&
	   highNote < CountOf(ProTrackerPeriodTable) + 36 + NOTE_MIN)
	{
		lowPeriod  = ProTrackerPeriodTable[lowNote - 36 - NOTE_MIN];
		highPeriod = ProTrackerPeriodTable[highNote - 36 - NOTE_MIN];

		// with a fixed speed of 6 ticks/row, and excluding the first row,
		// 1xx/2xx param has a max value of (low-high)/5 to avoid sliding too far
		return std::min<uint8>(value, static_cast<uint8>((lowPeriod - highPeriod) / 5));
	}

	return 0;
}

bool CSoundFile::ReadSFX(FileReader &file, ModLoadingFlags loadFlags)
//-------------------------------------------------------------------
{
	if(file.Seek(0x3C), file.ReadMagic("SONG"))
	{
		InitializeGlobals(MOD_TYPE_SFX);
		m_nSamples = 15;
	} else if(file.Seek(0x7C), file.ReadMagic("SO31"))
	{
		InitializeGlobals(MOD_TYPE_SFX);
		m_nSamples = 31;
	} else
	{
		return false;
	}

	uint32 sampleLen[31];

	file.Rewind();
	for(SAMPLEINDEX smp = 0; smp < m_nSamples; smp++)
	{
		sampleLen[smp] = file.ReadUint32BE();
		if(sampleLen[smp] > 131072)
			return false;
	}

	m_nChannels = 4;
	m_nInstruments = 0;
	m_nDefaultSpeed = 6;
	m_nMinPeriod = 14 * 4;
	m_nMaxPeriod = 3424 * 4;
	m_nSamplePreAmp = 64;
	m_SongFlags.reset();

	// Setup channel pan positions and volume
	SetupMODPanning(true);

	file.Skip(4);
	uint16 speed = file.ReadUint16BE();
	if(speed < 178)
		return false;
	m_nDefaultTempo = TEMPO((14565.0 * 122.0) / speed);

	file.Skip(14);

	uint32 invalidChars = 0;
	for(SAMPLEINDEX smp = 1; smp <= m_nSamples; smp++)
	{
		SFXSampleHeader sampleHeader;

		file.ReadConvertEndianness(sampleHeader);
		sampleHeader.ConvertToMPT(Samples[smp], sampleLen[smp - 1]);

		// Get rid of weird characters in sample names.
		for(uint32 i = 0; i < CountOf(sampleHeader.name); i++)
		{
			if(sampleHeader.name[i] > 0 && sampleHeader.name[i] < ' ')
			{
				sampleHeader.name[i] = ' ';
				invalidChars++;
			}
		}
		if(invalidChars >= 128)
			return false;
		mpt::String::Read<mpt::String::spacePadded>(m_szNames[smp], sampleHeader.name);
	}

	// Broken conversions of the "Operation Stealth" soundtrack (BOND23 / BOND32)
	// There is a converter that shifts all note values except FFFD (empty note) to the left by 1 bit,
	// but it should not do that for FFFE (STP) notes - as a consequence, they turn into pattern breaks (FFFC).
	const bool fixPatternBreaks = !strcmp(m_szNames[1], "BASSE2.AMI") || !strcmp(m_szNames[1], "PRA1.AMI");

	SFXFileHeader fileHeader;
	file.ReadStruct(fileHeader);

	if(fileHeader.numOrders > 128)
		return false;
	if(loadFlags == onlyVerifyHeader)
		return true;

	PATTERNINDEX numPatterns = 0;
	for(ORDERINDEX ord = 0; ord < fileHeader.numOrders; ord++)
	{
		numPatterns = std::max<PATTERNINDEX>(numPatterns, fileHeader.orderList[ord] + 1u);
	}

	if(fileHeader.restartPos < fileHeader.numOrders)
		Order.SetRestartPos(fileHeader.restartPos);
	else
		Order.SetRestartPos(0);

	Order.ReadFromArray(fileHeader.orderList, fileHeader.numOrders);

	// SFX v2 / MMS modules have 4 extra bytes here for some reason
	if(m_nSamples == 31)
		file.Skip(4);

	uint8 lastNote[4] = {0};
	uint8 slideTo[4] = {0};
	uint8 slideRate[4] = {0};

	// Reading patterns
	for(PATTERNINDEX pat = 0; pat < numPatterns; pat++)
	{
		if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, 64))
		{
			file.Skip(64 * 4 * 4);
			continue;
		}

		for(ROWINDEX row = 0; row < 64; row++)
		{
			PatternRow rowBase = Patterns[pat].GetpModCommand(row, 0);
			for(CHANNELINDEX chn = 0; chn < 4; chn++)
			{
				ModCommand &m = rowBase[chn];
				uint8 data[4];
				file.ReadArray(data);

				if(data[0] == 0xFF)
				{
					lastNote[chn] = slideRate[chn] = 0;

					if(fixPatternBreaks && data[1] == 0xFC)
						data[1] = 0xFE;

					switch(data[1])
					{
					case 0xFE: // STP (note cut)
						m.command = CMD_VOLUME;
						continue;
					case 0xFD: // PIC (null)
						continue;
					case 0xFC: // BRK (pattern break)
						m.command = CMD_PATTERNBREAK;
						continue;
					}
				}

				ReadMODPatternEntry(data, m);
				if(m.note != NOTE_NONE)
				{
					lastNote[chn] = m.note;
					slideRate[chn] = 0;
				}

				if(m.command || m.param)
				{
					switch(m.command)
					{
					case 0x1: // Arpeggio
						m.command = CMD_ARPEGGIO;
						break;

					case 0x2: // Portamento (like Ultimate Soundtracker)
						if(m.param & 0xF0)
						{
							m.command = CMD_PORTAMENTODOWN;
							m.param >>= 4;
						} else if(m.param & 0xF)
						{
							m.command = CMD_PORTAMENTOUP;
							m.param &= 0x0F;
						} else
						{
							m.command = m.param = 0;
						}
						break;

					case 0x3: // Enable LED filter
						// Give precedence to 7xy/8xy slides
						if(slideRate[chn])
						{
							m.command = m.param = 0;
							break;
						}
						m.command = CMD_MODCMDEX;
						m.param = 0;
						break;

					case 0x4: // Disable LED filter
						// Give precedence to 7xy/8xy slides
						if(slideRate[chn])
						{
							m.command = m.param = 0;
							break;
						}
						m.command = CMD_MODCMDEX;
						m.param = 1;
						break;

					case 0x5: // Increase volume
						if(m.instr)
						{
							m.command = CMD_VOLUME;
							m.param = std::min(ModCommand::PARAM(0x3F), static_cast<ModCommand::PARAM>((Samples[m.instr].nVolume / 4u) + m.param));

							// Give precedence to 7xy/8xy slides (and move this to the volume column)
							if(slideRate[chn])
							{
								m.volcmd = VOLCMD_VOLUME;
								m.vol = m.param;
								m.command = m.param = 0;
								break;
							}
						} else
						{
							m.command = m.param = 0;
						}
						break;

					case 0x6: // Decrease volume
						if(m.instr)
						{
							m.command = CMD_VOLUME;
							if((Samples[m.instr].nVolume / 4u) >= m.param)
								m.param = static_cast<ModCommand::PARAM>(Samples[m.instr].nVolume / 4u) - m.param;
							else
								m.param = 0;

							// Give precedence to 7xy/8xy slides (and move this to the volume column)
							if(slideRate[chn])
							{
								m.volcmd = VOLCMD_VOLUME;
								m.vol = m.param;
								m.command = m.param = 0;
								break;
							}
						} else
						{
							m.command = m.param = 0;
						}
						break;

					case 0x7: // 7xy: Slide down x semitones at speed y
						slideTo[chn] = lastNote[chn] - (m.param >> 4);

						m.command = CMD_PORTAMENTODOWN;
						slideRate[chn] = m.param & 0xF;
						m.param = ClampSlideParam(slideRate[chn], slideTo[chn], lastNote[chn]);
						break;

					case 0x8: // 8xy: Slide up x semitones at speed y
						slideTo[chn] = lastNote[chn] + (m.param >> 4);

						m.command = CMD_PORTAMENTOUP;
						slideRate[chn] = m.param & 0xF;
						m.param = ClampSlideParam(slideRate[chn], lastNote[chn], slideTo[chn]);
						break;

					default:
						m.command = CMD_NONE;
						break;
					}
				}

				// Continue 7xy/8xy slides if needed
				if(m.command == CMD_NONE && slideRate[chn])
				{
					if(slideTo[chn])
					{
						m.note = lastNote[chn] = slideTo[chn];
						m.param = slideRate[chn];
						slideTo[chn] = 0;
					}
					m.command = CMD_TONEPORTAMENTO;
				}
			}
		}
	}

	// Reading samples
	if(loadFlags & loadSampleData)
	{
		for(SAMPLEINDEX smp = 1; smp <= m_nSamples; smp++) if(Samples[smp].nLength)
		{
			SampleIO(
				SampleIO::_8bit,
				SampleIO::mono,
				SampleIO::littleEndian,
				SampleIO::signedPCM)
				.ReadSample(Samples[smp], file);
		}
	}

	return true;
}

OPENMPT_NAMESPACE_END
