/*
 * Load_plm.cpp
 * ------------
 * Purpose: PLM (Disorder Tracker 2) module loader
 * 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 "Loaders.h"


OPENMPT_NAMESPACE_BEGIN

struct PLMFileHeader
{
	char     magic[4];		// "PLM\x1A"
	uint8le  headerSize;	// Number of bytes in header, including magic bytes
	uint8le  version;		// version code of file format (0x10)
	char     songName[48];
	uint8le  numChannels;
	uint8le  flags;			// unused?
	uint8le  maxVol;		// Maximum volume for vol slides, normally 0x40
	uint8le  amplify;		// SoundBlaster amplify, 0x40 = no amplify
	uint8le  tempo;
	uint8le  speed;
	uint8le  panPos[32];	// 0...15
	uint8le  numSamples;
	uint8le  numPatterns;
	uint16le numOrders;
};

MPT_BINARY_STRUCT(PLMFileHeader, 96)


struct PLMSampleHeader
{
	enum SampleFlags
	{
		smp16Bit = 1,
		smpPingPong = 2,
	};

	char     magic[4];		// "PLS\x1A"
	uint8le  headerSize;	// Number of bytes in header, including magic bytes
	uint8le  version;	
	char     name[32];
	char     filename[12];
	uint8le  panning;		// 0...15, 255 = no pan
	uint8le  volume;		// 0...64
	uint8le  flags;			// See SampleFlags
	uint16le sampleRate;
	char     unused[4];
	uint32le loopStart;
	uint32le loopEnd;
	uint32le length;
};

MPT_BINARY_STRUCT(PLMSampleHeader, 71)


struct PLMPatternHeader
{
	uint32le size;
	uint8le  numRows;
	uint8le  numChannels;
	uint8le  color;
	char     name[25];
};

MPT_BINARY_STRUCT(PLMPatternHeader, 32)


struct PLMOrderItem
{
	uint16le x;		// Starting position of pattern
	uint8le  y;		// Number of first channel
	uint8le  pattern;
};

MPT_BINARY_STRUCT(PLMOrderItem, 4)


static bool ValidateHeader(const PLMFileHeader &fileHeader)
{
	if(std::memcmp(fileHeader.magic, "PLM\x1A", 4)
		|| fileHeader.version != 0x10
		|| fileHeader.numChannels == 0 || fileHeader.numChannels > 32
		|| fileHeader.headerSize < sizeof(PLMFileHeader)
		)
	{
		return false;
	}
	return true;
}


static uint64 GetHeaderMinimumAdditionalSize(const PLMFileHeader &fileHeader)
{
	return fileHeader.headerSize - sizeof(PLMFileHeader) + 4 * (fileHeader.numOrders + fileHeader.numPatterns + fileHeader.numSamples);
}


CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderPLM(MemoryFileReader file, const uint64 *pfilesize)
{
	PLMFileHeader fileHeader;
	if(!file.ReadStruct(fileHeader))
	{
		return ProbeWantMoreData;
	}
	if(!ValidateHeader(fileHeader))
	{
		return ProbeFailure;
	}
	return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader));
}


bool CSoundFile::ReadPLM(FileReader &file, ModLoadingFlags loadFlags)
{
	file.Rewind();

	PLMFileHeader fileHeader;
	if(!file.ReadStruct(fileHeader))
	{
		return false;
	}
	if(!ValidateHeader(fileHeader))
	{
		return false;
	}
	if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader))))
	{
		return false;
	}
	if(loadFlags == onlyVerifyHeader)
	{
		return true;
	}

	if(!file.Seek(fileHeader.headerSize))
	{
		return false;
	}

	InitializeGlobals(MOD_TYPE_PLM);
	InitializeChannels();
	m_SongFlags = SONG_ITOLDEFFECTS;
	m_playBehaviour.set(kApplyOffsetWithoutNote);

	m_modFormat.formatName = U_("Disorder Tracker 2");
	m_modFormat.type = U_("plm");
	m_modFormat.charset = mpt::Charset::CP437;

	// Some PLMs use ASCIIZ, some space-padding strings...weird. Oh, and the file browser stops at 0 bytes in the name, the main GUI doesn't.
	m_songName = mpt::String::ReadBuf(mpt::String::spacePadded, fileHeader.songName);
	m_nChannels = fileHeader.numChannels + 1;	// Additional channel for writing pattern breaks
	m_nSamplePreAmp = fileHeader.amplify;
	m_nDefaultTempo.Set(fileHeader.tempo);
	m_nDefaultSpeed = fileHeader.speed;
	for(CHANNELINDEX chn = 0; chn < fileHeader.numChannels; chn++)
	{
		ChnSettings[chn].nPan = fileHeader.panPos[chn] * 0x11;
	}
	m_nSamples = fileHeader.numSamples;

	std::vector<PLMOrderItem> order(fileHeader.numOrders);
	file.ReadVector(order, fileHeader.numOrders);

	std::vector<uint32le> patternPos, samplePos;
	file.ReadVector(patternPos, fileHeader.numPatterns);
	file.ReadVector(samplePos, fileHeader.numSamples);

	for(SAMPLEINDEX smp = 0; smp < fileHeader.numSamples; smp++)
	{
		ModSample &sample = Samples[smp + 1];
		sample.Initialize();

		PLMSampleHeader sampleHeader;
		if(samplePos[smp] == 0
			|| !file.Seek(samplePos[smp])
			|| !file.ReadStruct(sampleHeader))
				continue;

		m_szNames[smp + 1] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.name);
		sample.filename = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.filename);
		if(sampleHeader.panning <= 15)
		{
			sample.uFlags.set(CHN_PANNING);
			sample.nPan = sampleHeader.panning * 0x11;
		}
		sample.nGlobalVol = std::min(sampleHeader.volume.get(), uint8(64));
		sample.nC5Speed = sampleHeader.sampleRate;
		sample.nLoopStart = sampleHeader.loopStart;
		sample.nLoopEnd = sampleHeader.loopEnd;
		sample.nLength = sampleHeader.length;
		if(sampleHeader.flags & PLMSampleHeader::smp16Bit)
		{
			sample.nLoopStart /= 2;
			sample.nLoopEnd /= 2;
			sample.nLength /= 2;
		}
		if(sample.nLoopEnd > sample.nLoopStart)
		{
			sample.uFlags.set(CHN_LOOP);
			if(sampleHeader.flags & PLMSampleHeader::smpPingPong) sample.uFlags.set(CHN_PINGPONGLOOP);
		}
		sample.SanitizeLoops();
		
		if(loadFlags & loadSampleData)
		{
			file.Seek(samplePos[smp] + sampleHeader.headerSize);
			SampleIO(
				(sampleHeader.flags & PLMSampleHeader::smp16Bit) ? SampleIO::_16bit : SampleIO::_8bit,
				SampleIO::mono,
				SampleIO::littleEndian,
				SampleIO::unsignedPCM)
				.ReadSample(sample, file);
		}
	}

	if(!(loadFlags & loadPatternData))
	{
		return true;
	}

	// PLM is basically one huge continuous pattern, so we split it up into smaller patterns.
	const ROWINDEX rowsPerPat = 64;
	uint32 maxPos = 0;

	static constexpr EffectCommand effTrans[] =
	{
		CMD_NONE,
		CMD_PORTAMENTOUP,
		CMD_PORTAMENTODOWN,
		CMD_TONEPORTAMENTO,
		CMD_VOLUMESLIDE,
		CMD_TREMOLO,
		CMD_VIBRATO,
		CMD_S3MCMDEX,		// Tremolo Waveform
		CMD_S3MCMDEX,		// Vibrato Waveform
		CMD_TEMPO,
		CMD_SPEED,
		CMD_POSITIONJUMP,	// Jump to order
		CMD_POSITIONJUMP,	// Break to end of this order
		CMD_OFFSET,
		CMD_S3MCMDEX,		// GUS Panning
		CMD_RETRIG,
		CMD_S3MCMDEX,		// Note Delay
		CMD_S3MCMDEX,		// Note Cut
		CMD_S3MCMDEX,		// Pattern Delay
		CMD_FINEVIBRATO,
		CMD_VIBRATOVOL,
		CMD_TONEPORTAVOL,
		CMD_OFFSETPERCENTAGE,
	};

	Order().clear();
	for(const auto &ord : order)
	{
		if(ord.pattern >= fileHeader.numPatterns
			|| ord.y > fileHeader.numChannels
			|| !file.Seek(patternPos[ord.pattern])) continue;

		PLMPatternHeader patHeader;
		file.ReadStruct(patHeader);
		if(!patHeader.numRows) continue;

		static_assert(ORDERINDEX_MAX >= (std::numeric_limits<decltype(ord.x)>::max() + 255) / rowsPerPat);
		ORDERINDEX curOrd = static_cast<ORDERINDEX>(ord.x / rowsPerPat);
		ROWINDEX curRow = static_cast<ROWINDEX>(ord.x % rowsPerPat);
		const CHANNELINDEX numChannels = std::min(patHeader.numChannels.get(), static_cast<uint8>(fileHeader.numChannels - ord.y));
		const uint32 patternEnd = ord.x + patHeader.numRows;
		maxPos = std::max(maxPos, patternEnd);

		ModCommand::NOTE lastNote[32] = { 0 };
		for(ROWINDEX r = 0; r < patHeader.numRows; r++, curRow++)
		{
			if(curRow >= rowsPerPat)
			{
				curRow = 0;
				curOrd++;
			}
			if(curOrd >= Order().size())
			{
				Order().resize(curOrd + 1);
				Order()[curOrd] = Patterns.InsertAny(rowsPerPat);
			}
			PATTERNINDEX pat = Order()[curOrd];
			if(!Patterns.IsValidPat(pat)) break;

			ModCommand *m = Patterns[pat].GetpModCommand(curRow, ord.y);
			for(CHANNELINDEX c = 0; c < numChannels; c++, m++)
			{
				const auto [note, instr, volume, command, param] = file.ReadArray<uint8, 5>();
				if(note > 0 && note < 0x90)
					lastNote[c] = m->note = (note >> 4) * 12 + (note & 0x0F) + 12 + NOTE_MIN;
				else
					m->note = NOTE_NONE;
				m->instr = instr;
				m->volcmd = VOLCMD_VOLUME;
				if(volume != 0xFF)
					m->vol = volume;
				else
					m->volcmd = VOLCMD_NONE;

				if(command < std::size(effTrans))
				{
					m->command = effTrans[command];
					m->param = param;
					// Fix some commands
					switch(command)
					{
					case 0x07:	// Tremolo waveform
						m->param = 0x40 | (m->param & 0x03);
						break;
					case 0x08:	// Vibrato waveform
						m->param = 0x30 | (m->param & 0x03);
						break;
					case 0x0B:	// Jump to order
						if(m->param < order.size())
						{
							uint16 target = order[m->param].x;
							m->param = static_cast<ModCommand::PARAM>(target / rowsPerPat);
							ModCommand *mBreak = Patterns[pat].GetpModCommand(curRow, m_nChannels - 1);
							mBreak->command = CMD_PATTERNBREAK;
							mBreak->param = static_cast<ModCommand::PARAM>(target % rowsPerPat);
						}
						break;
					case 0x0C:	// Jump to end of order
						{
							m->param = static_cast<ModCommand::PARAM>(patternEnd / rowsPerPat);
							ModCommand *mBreak = Patterns[pat].GetpModCommand(curRow, m_nChannels - 1);
							mBreak->command = CMD_PATTERNBREAK;
							mBreak->param = static_cast<ModCommand::PARAM>(patternEnd % rowsPerPat);
						}
						break;
					case 0x0E:	// GUS Panning
						m->param = 0x80 | (m->param & 0x0F);
						break;
					case 0x10:	// Delay Note
						m->param = 0xD0 | std::min(m->param, ModCommand::PARAM(0x0F));
						break;
					case 0x11:	// Cut Note
						m->param = 0xC0 | std::min(m->param, ModCommand::PARAM(0x0F));
						break;
					case 0x12:	// Pattern Delay
						m->param = 0xE0 | std::min(m->param, ModCommand::PARAM(0x0F));
						break;
					case 0x04:	// Volume Slide
					case 0x14:	// Vibrato + Volume Slide
					case 0x15:	// Tone Portamento + Volume Slide
						// If both nibbles of a volume slide are set, act as fine volume slide up
						if((m->param & 0xF0) && (m->param & 0x0F) && (m->param & 0xF0) != 0xF0)
						{
							m->param |= 0x0F;
						}
						break;
					case 0x0D:
					case 0x16:
						// Offset without note
						if(m->note == NOTE_NONE)
						{
							m->note = lastNote[c];
						}
						break;
					}
				}
			}
			if(patHeader.numChannels > numChannels)
			{
				file.Skip(5 * (patHeader.numChannels - numChannels));
			}
		}
	}
	// Module ends with the last row of the last order item
	ROWINDEX endPatSize = maxPos % rowsPerPat;
	ORDERINDEX endOrder = static_cast<ORDERINDEX>(maxPos / rowsPerPat);
	if(endPatSize > 0 && Order().IsValidPat(endOrder))
	{
		Patterns[Order()[endOrder]].Resize(endPatSize, false);
	}
	// If there are still any non-existent patterns in our order list, insert some blank patterns.
	PATTERNINDEX blankPat = PATTERNINDEX_INVALID;
	for(auto &pat : Order())
	{
		if(pat == Order.GetInvalidPatIndex())
		{
			if(blankPat == PATTERNINDEX_INVALID)
			{
				blankPat = Patterns.InsertAny(rowsPerPat);
			}
			pat = blankPat;
		}
	}

	return true;
}

OPENMPT_NAMESPACE_END
