/*
 * Load_imf.cpp
 * ------------
 * Purpose: IMF (Imago Orpheus) module loader
 * Notes  : Reverb and Chorus are not supported.
 * Authors: Storlek (Original author - http://schismtracker.org/ - code ported with permission)
 *          Johannes Schultz (OpenMPT Port, tweaks)
 * 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 IMFChannel
{
	char  name[12];  // Channel name (ASCIIZ-String, max 11 chars)
	uint8 chorus;    // Default chorus
	uint8 reverb;    // Default reverb
	uint8 panning;   // Pan positions 00-FF
	uint8 status;    // Channel status: 0 = enabled, 1 = mute, 2 = disabled (ignore effects!)
};

MPT_BINARY_STRUCT(IMFChannel, 16)

struct IMFFileHeader
{
	enum SongFlags
	{
		linearSlides = 0x01,
	};

	char     title[32];  // Songname (ASCIIZ-String, max. 31 chars)
	uint16le ordNum;     // Number of orders saved
	uint16le patNum;     // Number of patterns saved
	uint16le insNum;     // Number of instruments saved
	uint16le flags;      // See SongFlags
	uint8le  unused1[8];
	uint8le  tempo;   // Default tempo (Axx, 1...255)
	uint8le  bpm;     // Default beats per minute (BPM) (Txx, 32...255)
	uint8le  master;  // Default master volume (Vxx, 0...64)
	uint8le  amp;     // Amplification factor (mixing volume, 4...127)
	uint8le  unused2[8];
	char     im10[4];         // 'IM10'
	IMFChannel channels[32];  // Channel settings
};

MPT_BINARY_STRUCT(IMFFileHeader, 576)

struct IMFEnvelope
{
	enum EnvFlags
	{
		envEnabled = 0x01,
		envSustain = 0x02,
		envLoop    = 0x04,
	};

	uint8 points;     // Number of envelope points
	uint8 sustain;    // Envelope sustain point
	uint8 loopStart;  // Envelope loop start point
	uint8 loopEnd;    // Envelope loop end point
	uint8 flags;      // See EnvFlags
	uint8 unused[3];
};

MPT_BINARY_STRUCT(IMFEnvelope, 8)

struct IMFEnvNode
{
	uint16le tick;
	uint16le value;
};

MPT_BINARY_STRUCT(IMFEnvNode, 4)

struct IMFInstrument
{
	enum EnvTypes
	{
		volEnv    = 0,
		panEnv    = 1,
		filterEnv = 2,
	};

	char        name[32];  // Inst. name (ASCIIZ-String, max. 31 chars)
	uint8le     map[120];  // Multisample settings
	uint8le     unused[8];
	IMFEnvNode  nodes[3][16];
	IMFEnvelope env[3];
	uint16le    fadeout;  // Fadeout rate (0...0FFFH)
	uint16le    smpNum;   // Number of samples in instrument
	char        ii10[4];  // 'II10' (not verified by Orpheus)

	void ConvertEnvelope(InstrumentEnvelope &mptEnv, EnvTypes e) const
	{
		const uint8 shift = (e == volEnv) ? 0 : 2;
		const uint8 mirror = (e == filterEnv) ? 0xFF : 0x00;

		mptEnv.dwFlags.set(ENV_ENABLED, (env[e].flags & 1) != 0);
		mptEnv.dwFlags.set(ENV_SUSTAIN, (env[e].flags & 2) != 0);
		mptEnv.dwFlags.set(ENV_LOOP, (env[e].flags & 4) != 0);

		mptEnv.resize(Clamp(env[e].points, uint8(2), uint8(16)));
		mptEnv.nLoopStart = env[e].loopStart;
		mptEnv.nLoopEnd = env[e].loopEnd;
		mptEnv.nSustainStart = mptEnv.nSustainEnd = env[e].sustain;

		uint16 minTick = 0; // minimum tick value for next node
		for(uint32 n = 0; n < mptEnv.size(); n++)
		{
			mptEnv[n].tick = minTick = std::max(minTick, nodes[e][n].tick.get());
			minTick++;
			uint8 value = static_cast<uint8>(nodes[e][n].value ^ mirror) >> shift;
			mptEnv[n].value = std::min(value, uint8(ENVELOPE_MAX));
		}
		mptEnv.Convert(MOD_TYPE_XM, MOD_TYPE_IT);
	}

	// Convert an IMFInstrument to OpenMPT's internal instrument representation.
	void ConvertToMPT(ModInstrument &mptIns, SAMPLEINDEX firstSample) const
	{
		mptIns.name = mpt::String::ReadBuf(mpt::String::nullTerminated, name);

		if(smpNum)
		{
			for(size_t note = 0; note < std::min(std::size(map), std::size(mptIns.Keyboard) - 12u); note++)
			{
				mptIns.Keyboard[note + 12] = firstSample + map[note];
			}
		}

		mptIns.nFadeOut = fadeout;
		mptIns.midiPWD = 1;  // For CMD_FINETUNE

		ConvertEnvelope(mptIns.VolEnv, volEnv);
		ConvertEnvelope(mptIns.PanEnv, panEnv);
		ConvertEnvelope(mptIns.PitchEnv, filterEnv);
		if(mptIns.PitchEnv.dwFlags[ENV_ENABLED])
			mptIns.PitchEnv.dwFlags.set(ENV_FILTER);

		// hack to get === to stop notes
		if(!mptIns.VolEnv.dwFlags[ENV_ENABLED] && !mptIns.nFadeOut)
			mptIns.nFadeOut = 32767;
	}
};

MPT_BINARY_STRUCT(IMFInstrument, 384)

struct IMFSample
{
	enum SampleFlags
	{
		smpLoop			= 0x01,
		smpPingPongLoop	= 0x02,
		smp16Bit		= 0x04,
		smpPanning		= 0x08,
	};

	char     filename[13];  // Sample filename (12345678.ABC) */
	uint8le  unused1[3];
	uint32le length;        // Length (in bytes)
	uint32le loopStart;     // Loop start (in bytes)
	uint32le loopEnd;       // Loop end (in bytes)
	uint32le c5Speed;       // Samplerate
	uint8le  volume;        // Default volume (0...64)
	uint8le  panning;       // Default pan (0...255)
	uint8le  unused2[14];
	uint8le  flags;  // Sample flags
	uint8le  unused3[5];
	uint16le ems;      // Reserved for internal usage
	uint32le dram;     // Reserved for internal usage
	char     is10[4];  // 'IS10'

	// Convert an IMFSample to OpenMPT's internal sample representation.
	void ConvertToMPT(ModSample &mptSmp) const
	{
		mptSmp.Initialize();
		mptSmp.filename = mpt::String::ReadBuf(mpt::String::nullTerminated, filename);

		mptSmp.nLength = length;
		mptSmp.nLoopStart = loopStart;
		mptSmp.nLoopEnd = loopEnd;
		mptSmp.nC5Speed = c5Speed;
		mptSmp.nVolume = volume * 4;
		mptSmp.nPan = panning;
		if(flags & smpLoop)
			mptSmp.uFlags.set(CHN_LOOP);
		if(flags & smpPingPongLoop)
			mptSmp.uFlags.set(CHN_PINGPONGLOOP);
		if(flags & smp16Bit)
		{
			mptSmp.uFlags.set(CHN_16BIT);
			mptSmp.nLength /= 2;
			mptSmp.nLoopStart /= 2;
			mptSmp.nLoopEnd /= 2;
		}
		if(flags & smpPanning)
			mptSmp.uFlags.set(CHN_PANNING);
	}
};

MPT_BINARY_STRUCT(IMFSample, 64)


static constexpr EffectCommand imfEffects[] =
{
	CMD_NONE,
	CMD_SPEED,           // 0x01 1xx Set Tempo
	CMD_TEMPO,           // 0x02 2xx Set BPM
	CMD_TONEPORTAMENTO,  // 0x03 3xx Tone Portamento
	CMD_TONEPORTAVOL,    // 0x04 4xy Tone Portamento + Volume Slide
	CMD_VIBRATO,         // 0x05 5xy Vibrato
	CMD_VIBRATOVOL,      // 0x06 6xy Vibrato + Volume Slide
	CMD_FINEVIBRATO,     // 0x07 7xy Fine Vibrato
	CMD_TREMOLO,         // 0x08 8xy Tremolo
	CMD_ARPEGGIO,        // 0x09 9xy Arpeggio
	CMD_PANNING8,        // 0x0A Axx Set Pan Position
	CMD_PANNINGSLIDE,    // 0x0B Bxy Pan Slide
	CMD_VOLUME,          // 0x0C Cxx Set Volume
	CMD_VOLUMESLIDE,     // 0x0D Dxy Volume Slide
	CMD_VOLUMESLIDE,     // 0x0E Exy Fine Volume Slide
	CMD_FINETUNE,        // 0x0F Fxx Set Finetune
	CMD_NOTESLIDEUP,     // 0x10 Gxy Note Slide Up
	CMD_NOTESLIDEDOWN,   // 0x11 Hxy Note Slide Down
	CMD_PORTAMENTOUP,    // 0x12 Ixx Slide Up
	CMD_PORTAMENTODOWN,  // 0x13 Jxx Slide Down
	CMD_PORTAMENTOUP,    // 0x14 Kxx Fine Slide Up
	CMD_PORTAMENTODOWN,  // 0x15 Lxx Fine Slide Down
	CMD_MIDI,            // 0x16 Mxx Set Filter Cutoff
	CMD_MIDI,            // 0x17 Nxy Filter Slide + Resonance
	CMD_OFFSET,          // 0x18 Oxx Set Sample Offset
	CMD_NONE,            // 0x19 Pxx Set Fine Sample Offset - XXX
	CMD_KEYOFF,          // 0x1A Qxx Key Off
	CMD_RETRIG,          // 0x1B Rxy Retrig
	CMD_TREMOR,          // 0x1C Sxy Tremor
	CMD_POSITIONJUMP,    // 0x1D Txx Position Jump
	CMD_PATTERNBREAK,    // 0x1E Uxx Pattern Break
	CMD_GLOBALVOLUME,    // 0x1F Vxx Set Mastervolume
	CMD_GLOBALVOLSLIDE,  // 0x20 Wxy Mastervolume Slide
	CMD_S3MCMDEX,        // 0x21 Xxx Extended Effect
	                     //      X1x Set Filter
	                     //      X3x Glissando
	                     //      X5x Vibrato Waveform
	                     //      X8x Tremolo Waveform
	                     //      XAx Pattern Loop
	                     //      XBx Pattern Delay
	                     //      XCx Note Cut
	                     //      XDx Note Delay
	                     //      XEx Ignore Envelope
	                     //      XFx Invert Loop
	CMD_NONE,            // 0x22 Yxx Chorus - XXX
	CMD_NONE,            // 0x23 Zxx Reverb - XXX
};

static void ImportIMFEffect(ModCommand &m)
{
	uint8 n;
	// fix some of them
	switch(m.command)
	{
	case 0xE: // fine volslide
		// hackaround to get almost-right behavior for fine slides (i think!)
		if(m.param == 0)
			/* nothing */;
		else if(m.param == 0xF0)
			m.param = 0xEF;
		else if(m.param == 0x0F)
			m.param = 0xFE;
		else if(m.param & 0xF0)
			m.param |= 0x0F;
		else
			m.param |= 0xF0;
		break;
	case 0xF: // set finetune
		m.param ^= 0x80;
		break;
	case 0x14: // fine slide up
	case 0x15: // fine slide down
		// this is about as close as we can do...
		if(m.param >> 4)
			m.param = 0xF0 | (m.param >> 4);
		else
			m.param |= 0xE0;
		break;
	case 0x16: // cutoff
		m.param = (0xFF - m.param) / 2u;
		break;
	case 0x17: // cutoff slide + resonance (TODO: cutoff slide is currently not handled)
		m.param = 0x80 | (m.param & 0x0F);
		break;
	case 0x1F: // set global volume
		m.param = mpt::saturate_cast<uint8>(m.param * 2);
		break;
	case 0x21:
		n = 0;
		switch (m.param >> 4)
		{
		case 0:
			/* undefined, but since S0x does nothing in IT anyway, we won't care.
			this is here to allow S00 to pick up the previous value (assuming IMF
			even does that -- I haven't actually tried it) */
			break;
		default: // undefined
		case 0x1: // set filter
		case 0xF: // invert loop
			m.command = CMD_NONE;
			break;
		case 0x3: // glissando
			n = 0x20;
			break;
		case 0x5: // vibrato waveform
			n = 0x30;
			break;
		case 0x8: // tremolo waveform
			n = 0x40;
			break;
		case 0xA: // pattern loop
			n = 0xB0;
			break;
		case 0xB: // pattern delay
			n = 0xE0;
			break;
		case 0xC: // note cut
		case 0xD: // note delay
			// Apparently, Imago Orpheus doesn't cut samples on tick 0.
			if(!m.param)
				m.command = CMD_NONE;
			break;
		case 0xE: // ignore envelope
			switch(m.param & 0x0F)
			{
			// All envelopes
			// Predicament: we can only disable one envelope at a time. Volume is probably most noticeable, so let's go with that.
			case 0: m.param = 0x77; break;
			// Volume
			case 1: m.param = 0x77; break;
			// Panning
			case 2: m.param = 0x79; break;
			// Filter
			case 3: m.param = 0x7B; break;
			}
			break;
		case 0x18: // sample offset
			// O00 doesn't pick up the previous value
			if(!m.param)
				m.command = CMD_NONE;
			break;
		}
		if(n)
			m.param = n | (m.param & 0x0F);
		break;
	}
	m.command = (m.command < std::size(imfEffects)) ? imfEffects[m.command] : CMD_NONE;
	if(m.command == CMD_VOLUME && m.volcmd == VOLCMD_NONE)
	{
		m.volcmd = VOLCMD_VOLUME;
		m.vol = m.param;
		m.command = CMD_NONE;
		m.param = 0;
	}
}


static bool ValidateHeader(const IMFFileHeader &fileHeader)
{
	if(std::memcmp(fileHeader.im10, "IM10", 4)
	   || fileHeader.ordNum > 256
	   || fileHeader.insNum >= MAX_INSTRUMENTS
	   || fileHeader.bpm < 32
	   || fileHeader.master > 64
	   || fileHeader.amp < 4
	   || fileHeader.amp > 127)
	{
		return false;
	}
	bool channelFound = false;
	for(const auto &chn : fileHeader.channels)
	{
		switch(chn.status)
		{
		case 0: // enabled; don't worry about it
			channelFound = true;
			break;
		case 1: // mute
			channelFound = true;
			break;
		case 2: // disabled
			// nothing
			break;
		default: // uhhhh.... freak out
			return false;
		}
	}
	if(!channelFound)
	{
		return false;
	}
	return true;
}


static uint64 GetHeaderMinimumAdditionalSize(const IMFFileHeader &fileHeader)
{
	return 256 + fileHeader.patNum * 4 + fileHeader.insNum * sizeof(IMFInstrument);
}


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


bool CSoundFile::ReadIMF(FileReader &file, ModLoadingFlags loadFlags)
{
	IMFFileHeader fileHeader;
	file.Rewind();
	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;
	}

	// Read channel configuration
	std::bitset<32> ignoreChannels; // bit set for each channel that's completely disabled
	uint8 detectedChannels = 0;
	for(uint8 chn = 0; chn < 32; chn++)
	{
		ChnSettings[chn].Reset();
		ChnSettings[chn].nPan = fileHeader.channels[chn].panning * 256 / 255;

		ChnSettings[chn].szName = mpt::String::ReadBuf(mpt::String::nullTerminated, fileHeader.channels[chn].name);

		// TODO: reverb/chorus?
		switch(fileHeader.channels[chn].status)
		{
		case 0: // enabled; don't worry about it
			detectedChannels = chn + 1;
			break;
		case 1: // mute
			ChnSettings[chn].dwFlags = CHN_MUTE;
			detectedChannels = chn + 1;
			break;
		case 2: // disabled
			ChnSettings[chn].dwFlags = CHN_MUTE;
			ignoreChannels[chn] = true;
			break;
		default: // uhhhh.... freak out
			return false;
		}
	}

	InitializeGlobals(MOD_TYPE_IMF);
	m_nChannels = detectedChannels;

	m_modFormat.formatName = U_("Imago Orpheus");
	m_modFormat.type = U_("imf");
	m_modFormat.charset = mpt::Charset::CP437;

	//From mikmod: work around an Orpheus bug
	if(fileHeader.channels[0].status == 0)
	{
		CHANNELINDEX chn;
		for(chn = 1; chn < 16; chn++)
			if(fileHeader.channels[chn].status != 1)
				break;
		if(chn == 16)
			for(chn = 1; chn < 16; chn++)
				ChnSettings[chn].dwFlags.reset(CHN_MUTE);
	}

	// Song Name
	m_songName = mpt::String::ReadBuf(mpt::String::nullTerminated, fileHeader.title);

	m_SongFlags.set(SONG_LINEARSLIDES, fileHeader.flags & IMFFileHeader::linearSlides);
	m_nDefaultSpeed = fileHeader.tempo;
	m_nDefaultTempo.Set(fileHeader.bpm);
	m_nDefaultGlobalVolume = fileHeader.master * 4u;
	m_nSamplePreAmp = fileHeader.amp;

	m_nInstruments = fileHeader.insNum;
	m_nSamples = 0; // Will be incremented later

	uint8 orders[256];
	file.ReadArray(orders);
	ReadOrderFromArray(Order(), orders, fileHeader.ordNum, uint16_max, 0xFF);

	// Read patterns
	if(loadFlags & loadPatternData)
		Patterns.ResizeArray(fileHeader.patNum);
	for(PATTERNINDEX pat = 0; pat < fileHeader.patNum; pat++)
	{
		const uint16 length = file.ReadUint16LE(), numRows = file.ReadUint16LE();
		FileReader patternChunk = file.ReadChunk(length - 4);

		if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, numRows))
		{
			continue;
		}

		ModCommand dummy;
		ROWINDEX row = 0;
		while(row < numRows)
		{
			uint8 mask = patternChunk.ReadUint8();
			if(mask == 0)
			{
				row++;
				continue;
			}

			uint8 channel = mask & 0x1F;
			ModCommand &m = (channel < GetNumChannels()) ? *Patterns[pat].GetpModCommand(row, channel) : dummy;

			if(mask & 0x20)
			{
				// Read note/instrument
				const auto [note, instr] = patternChunk.ReadArray<uint8, 2>();
				m.note = note;
				m.instr = instr;

				if(m.note == 160)
				{
					m.note = NOTE_KEYOFF;
				} else if(m.note == 255)
				{
					m.note = NOTE_NONE;
				} else
				{
					m.note = (m.note >> 4) * 12 + (m.note & 0x0F) + 12 + 1;
					if(!m.IsNoteOrEmpty())
					{
						m.note = NOTE_NONE;
					}
				}
			}
			if((mask & 0xC0) == 0xC0)
			{
				// Read both effects and figure out what to do with them
				const auto [e1c, e1d, e2c, e2d] = patternChunk.ReadArray<uint8, 4>();  // Command 1, Data 1, Command 2, Data 2

				if(e1c == 0x0C)
				{
					m.vol = std::min(e1d, uint8(0x40));
					m.volcmd = VOLCMD_VOLUME;
					m.command = e2c;
					m.param = e2d;
				} else if(e2c == 0x0C)
				{
					m.vol = std::min(e2d, uint8(0x40));
					m.volcmd = VOLCMD_VOLUME;
					m.command = e1c;
					m.param = e1d;
				} else if(e1c == 0x0A)
				{
					m.vol = e1d * 64 / 255;
					m.volcmd = VOLCMD_PANNING;
					m.command = e2c;
					m.param = e2d;
				} else if(e2c == 0x0A)
				{
					m.vol = e2d * 64 / 255;
					m.volcmd = VOLCMD_PANNING;
					m.command = e1c;
					m.param = e1d;
				} else
				{
					/* check if one of the effects is a 'global' effect
					-- if so, put it in some unused channel instead.
					otherwise pick the most important effect. */
					m.command = e2c;
					m.param = e2d;
				}
			} else if(mask & 0xC0)
			{
				// There's one effect, just stick it in the effect column
				const auto [command, param] = patternChunk.ReadArray<uint8, 2>();
				m.command = command;
				m.param = param;
			}
			if(m.command)
				ImportIMFEffect(m);
			if(ignoreChannels[channel] && m.IsGlobalCommand())
				m.command = CMD_NONE;
		}
	}

	SAMPLEINDEX firstSample = 1; // first sample index of the current instrument

	// read instruments
	for(INSTRUMENTINDEX ins = 0; ins < GetNumInstruments(); ins++)
	{
		ModInstrument *instr = AllocateInstrument(ins + 1);
		IMFInstrument instrumentHeader;
		if(!file.ReadStruct(instrumentHeader) || instr == nullptr)
		{
			continue;
		}

		// Orpheus does not check this!
		//if(memcmp(instrumentHeader.ii10, "II10", 4) != 0)
		//	return false;
		instrumentHeader.ConvertToMPT(*instr, firstSample);

		// Read this instrument's samples
		for(SAMPLEINDEX smp = 0; smp < instrumentHeader.smpNum; smp++)
		{
			IMFSample sampleHeader;
			file.ReadStruct(sampleHeader);

			const SAMPLEINDEX smpID = firstSample + smp;
			if(memcmp(sampleHeader.is10, "IS10", 4) || smpID >= MAX_SAMPLES)
			{
				continue;
			}

			m_nSamples = smpID;
			ModSample &sample = Samples[smpID];

			sampleHeader.ConvertToMPT(sample);
			m_szNames[smpID] = sample.filename;

			if(sampleHeader.length)
			{
				FileReader sampleChunk = file.ReadChunk(sampleHeader.length);
				if(loadFlags & loadSampleData)
				{
					SampleIO(
						sample.uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit,
						SampleIO::mono,
						SampleIO::littleEndian,
						SampleIO::signedPCM)
						.ReadSample(sample, sampleChunk);
				}
			}
		}
		firstSample += instrumentHeader.smpNum;
	}

	return true;
}


OPENMPT_NAMESPACE_END
