/*
 * Load_ptm.cpp
 * ------------
 * Purpose: PTM (PolyTracker) module loader
 * 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 "Loaders.h"

OPENMPT_NAMESPACE_BEGIN

struct PTMFileHeader
{
	char     songname[28];		// Name of song, asciiz string
	uint8le  dosEOF;			// 26
	uint8le  versionLo;			// 03 version of file, currently 0203h
	uint8le  versionHi;			// 02
	uint8le  reserved1;			// Reserved, set to 0
	uint16le numOrders;			// Number of orders (0..256)
	uint16le numSamples;		// Number of instruments (1..255)
	uint16le numPatterns;		// Number of patterns (1..128)
	uint16le numChannels;		// Number of channels (voices) used (1..32)
	uint16le flags;				// Set to 0
	uint8le  reserved2[2];		// Reserved, set to 0
	char     magic[4];			// Song identification, 'PTMF'
	uint8le  reserved3[16];		// Reserved, set to 0
	uint8le  chnPan[32];		// Channel panning settings, 0..15, 0 = left, 7 = middle, 15 = right
	uint8le  orders[256];		// Order list, valid entries 0..nOrders-1
	uint16le patOffsets[128];	// Pattern offsets (*16)
};

MPT_BINARY_STRUCT(PTMFileHeader, 608)

struct PTMSampleHeader
{
	enum SampleFlags
	{
		smpTypeMask	= 0x03,
		smpPCM		= 0x01,

		smpLoop		= 0x04,
		smpPingPong	= 0x08,
		smp16Bit	= 0x10,
	};

	uint8le  flags;				// Sample type (see SampleFlags)
	char     filename[12];		// Name of external sample file
	uint8le  volume;			// Default volume
	uint16le c4speed;			// C-4 speed (yep, not C-5)
	uint8le  smpSegment[2];		// Sample segment (used internally)
	uint32le dataOffset;		// Offset of sample data
	uint32le length;			// Sample size (in bytes)
	uint32le loopStart;			// Start of loop
	uint32le loopEnd;			// End of loop
	uint8le  gusdata[14];
	char     samplename[28];	// Name of sample, ASCIIZ
	char     magic[4];			// Sample identification, 'PTMS'

	// Convert an PTM sample header to OpenMPT's internal sample header.
	SampleIO ConvertToMPT(ModSample &mptSmp) const
	{
		mptSmp.Initialize(MOD_TYPE_S3M);
		mptSmp.nVolume = std::min(volume.get(), uint8(64)) * 4;
		mptSmp.nC5Speed = c4speed * 2;

		mptSmp.filename = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, filename);

		SampleIO sampleIO(
			SampleIO::_8bit,
			SampleIO::mono,
			SampleIO::littleEndian,
			SampleIO::deltaPCM);

		if((flags & smpTypeMask) == smpPCM)
		{
			mptSmp.nLength = length;
			mptSmp.nLoopStart = loopStart;
			mptSmp.nLoopEnd = loopEnd;
			if(mptSmp.nLoopEnd > mptSmp.nLoopStart)
				mptSmp.nLoopEnd--;

			if(flags & smpLoop) mptSmp.uFlags.set(CHN_LOOP);
			if(flags & smpPingPong) mptSmp.uFlags.set(CHN_PINGPONGLOOP);
			if(flags & smp16Bit)
			{
				sampleIO |= SampleIO::_16bit;
				sampleIO |= SampleIO::PTM8Dto16;

				mptSmp.nLength /= 2;
				mptSmp.nLoopStart /= 2;
				mptSmp.nLoopEnd /= 2;
			}
		}

		return sampleIO;
	}
};

MPT_BINARY_STRUCT(PTMSampleHeader, 80)


static bool ValidateHeader(const PTMFileHeader &fileHeader)
{
	if(std::memcmp(fileHeader.magic, "PTMF", 4)
		|| fileHeader.dosEOF != 26
		|| fileHeader.versionHi > 2
		|| fileHeader.flags != 0
		|| !fileHeader.numChannels
		|| fileHeader.numChannels > 32
		|| !fileHeader.numOrders || fileHeader.numOrders > 256
		|| !fileHeader.numSamples || fileHeader.numSamples > 255
		|| !fileHeader.numPatterns || fileHeader.numPatterns > 128
		)
	{
		return false;
	}
	return true;
}


static uint64 GetHeaderMinimumAdditionalSize(const PTMFileHeader &fileHeader)
{
	return fileHeader.numSamples * sizeof(PTMSampleHeader);
}


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


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

	PTMFileHeader 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;
	}

	InitializeGlobals(MOD_TYPE_PTM);

	m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.songname);

	m_modFormat.formatName = U_("PolyTracker");
	m_modFormat.type = U_("ptm");
	m_modFormat.madeWithTracker = MPT_UFORMAT("PolyTracker {}.{}")(fileHeader.versionHi.get(), mpt::ufmt::hex0<2>(fileHeader.versionLo.get()));
	m_modFormat.charset = mpt::Charset::CP437;

	m_SongFlags = SONG_ITCOMPATGXX | SONG_ITOLDEFFECTS;
	m_nChannels = fileHeader.numChannels;
	m_nSamples = std::min(static_cast<SAMPLEINDEX>(fileHeader.numSamples), static_cast<SAMPLEINDEX>(MAX_SAMPLES - 1));
	ReadOrderFromArray(Order(), fileHeader.orders, fileHeader.numOrders, 0xFF, 0xFE);

	// Reading channel panning
	for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++)
	{
		ChnSettings[chn].Reset();
		ChnSettings[chn].nPan = ((fileHeader.chnPan[chn] & 0x0F) << 4) + 4;
	}

	// Reading samples
	FileReader sampleHeaderChunk = file.ReadChunk(fileHeader.numSamples * sizeof(PTMSampleHeader));
	for(SAMPLEINDEX smp = 0; smp < m_nSamples; smp++)
	{
		PTMSampleHeader sampleHeader;
		sampleHeaderChunk.ReadStruct(sampleHeader);

		ModSample &sample = Samples[smp + 1];
		m_szNames[smp + 1] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.samplename);
		SampleIO sampleIO = sampleHeader.ConvertToMPT(sample);

		if((loadFlags & loadSampleData) && sample.nLength && file.Seek(sampleHeader.dataOffset))
		{
			sampleIO.ReadSample(sample, file);
		}
	}

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

	Patterns.ResizeArray(fileHeader.numPatterns);
	for(PATTERNINDEX pat = 0; pat < fileHeader.numPatterns; pat++)
	{
		if(!Patterns.Insert(pat, 64)
			|| fileHeader.patOffsets[pat] == 0
			|| !file.Seek(fileHeader.patOffsets[pat] << 4))
		{
			continue;
		}

		ModCommand *rowBase = Patterns[pat].GetpModCommand(0, 0);
		ROWINDEX row = 0;
		while(row < 64 && file.CanRead(1))
		{
			uint8 b = file.ReadUint8();

			if(b == 0)
			{
				row++;
				rowBase += m_nChannels;
				continue;
			}
			CHANNELINDEX chn = (b & 0x1F);
			ModCommand dummy = ModCommand();
			ModCommand &m = chn < GetNumChannels() ? rowBase[chn] : dummy;

			if(b & 0x20)
			{
				const auto [note, instr] = file.ReadArray<uint8, 2>();
				m.note = note;
				m.instr = instr;
				if(m.note == 254)
					m.note = NOTE_NOTECUT;
				else if(!m.note || m.note > 120)
					m.note = NOTE_NONE;
			}
			if(b & 0x40)
			{
				const auto [command, param] = file.ReadArray<uint8, 2>();
				m.command = command;
				m.param = param;

				static constexpr EffectCommand effTrans[] = { CMD_GLOBALVOLUME, CMD_RETRIG, CMD_FINEVIBRATO, CMD_NOTESLIDEUP, CMD_NOTESLIDEDOWN, CMD_NOTESLIDEUPRETRIG, CMD_NOTESLIDEDOWNRETRIG, CMD_REVERSEOFFSET };
				if(m.command < 0x10)
				{
					// Beware: Effect letters are as in MOD, but portamento and volume slides behave like in S3M (i.e. fine slides share the same effect letters)
					ConvertModCommand(m);
				} else if(m.command < 0x10 + std::size(effTrans))
				{
					m.command = effTrans[m.command - 0x10];
				} else
				{
					m.command = CMD_NONE;
				}
				switch(m.command)
				{
				case CMD_PANNING8:
					// Don't be surprised about the strange formula, this is directly translated from original disassembly...
					m.command = CMD_S3MCMDEX;
					m.param = 0x80 | ((std::max<uint8>(m.param >> 3, 1u) - 1u) & 0x0F);
					break;
				case CMD_GLOBALVOLUME:
					m.param = std::min(m.param, uint8(0x40)) * 2u;
					break;
				}
			}
			if(b & 0x80)
			{
				m.volcmd = VOLCMD_VOLUME;
				m.vol = file.ReadUint8();
			}
		}
	}
	return true;
}


OPENMPT_NAMESPACE_END
