#include "Song.h"
#include "Effects.h"
#include "Instrument.h"
#include "Loaders.h"
#include "FSysLog.h"

/*
 * Channels: [1...8]
 * Patterns: [1...256]
 * Rows    : [1...255]
 * Orders  : [1...65535]
 * Samples : 36
 * Instr.  : 0
 * Tempo   : [32...255], T frames per 2.5 seconds.
 * Speed   : [1...31], S frames per row.
 * Notes   : [C-0...C-1...B-2]
 * Pitch   : Amiga periods mode, base is 428 units, period is divided by 2 each octave up.
 *           Slides as x/1 units per frame.
 * Volume  : [0...64] , linear or logarithmic.
 * Panning : [0...7].
 * Sampling: 8363 Hz.
 *
 * Notes:
 * Effects have no memory except for portamento and vibrato.
 * This means that effect value 0 as usually no effect.
 */

#define NoteDelta (Note_Central-12*1)

static const uint8_t Tag_MUSX[] = "MUSX";
static const uint8_t Tag_MVOX[] = "MVOX";
static const uint8_t Tag_STER[] = "STER";
static const uint8_t Tag_MNAM[] = "MNAM";
static const uint8_t Tag_ANAM[] = "ANAM";
static const uint8_t Tag_MLEN[] = "MLEN";
static const uint8_t Tag_PNUM[] = "PNUM";
static const uint8_t Tag_PLEN[] = "PLEN";
static const uint8_t Tag_SEQU[] = "SEQU";
static const uint8_t Tag_PATT[] = "PATT";
static const uint8_t Tag_SAMP[] = "SAMP";
static const uint8_t Tag_SNAM[] = "SNAM";
static const uint8_t Tag_SVOL[] = "SVOL";
static const uint8_t Tag_SLEN[] = "SLEN";
static const uint8_t Tag_ROFS[] = "ROFS";
static const uint8_t Tag_RLEN[] = "RLEN";
static const uint8_t Tag_SDAT[] = "SDAT";
static const uint8_t Typestr_MUSX[] = "Archimedes Tracker";
static const char Invalid_Block[] = "Invalid block";

#define TAG(x) (*(const uint32_t*) Tag_##x)

static void convert_effect(SongHdr* pSong, uint32_t channel, uint32_t effect, uint32_t value, uint32_t note)
{
#ifdef USELOG
	uint32_t oeffect = effect;
	uint32_t ovalue = value;
#endif

	switch(effect)
	{
		// MOD like effects
		case 0: // Pitch: Arpeggio
		case 1: // Pitch: Slide up
		case 2: // Pitch: Slide down
		case 3: // Pitch: Portamento
		case 4: // Pitch: Vibrato
		case 5: // Pitch: Portamento 0 - Volume: Slide
		case 6: // Pitch: Vibrato 0 - Volume: Slide
		case 7: // Volume: Vibrato (alias Tremolo)
		case 9: // Note: Set sample offset
		case 0xa: // Volume: Slide
		case 0xc: // Volume: Set
		{
			convert_effect_MOD(pSong, channel, effect, value, note);
		}
		break;
		case 0xe: // Special effects
		{
			switch (value >> 4)
			{
				case 0:
				case 8:
				case 0xf:
				{
					SysLog_BadEffect(pSong, 0, oeffect, ovalue);
				}
				break;
				default:
				{
					convert_effect_MOD(pSong, channel, effect, value, note);
				}
			}
		}
		break;
		// MUSX specific tags
		case 0xb: // Global: Break
		{
			uint32_t ten = value >> 4;
			value &= 0xf;
			if ((value > 9) || (ten > 9))
				SysLog_BadEffectValue(pSong, 0, oeffect, ovalue);
			else
			{
				value += ten * 10;
				Pattern_AddGlbEffect_Position(pSong, gcmd_pos_break, value);
			}
		}
		break;
		case 0x10: // g, Volume: Slide up, No memory
		{
			if (value) Pattern_AddEffect_NoteVolume(pSong, cmd_up + flag_cmd_slide_frameN0, value << 2);
		}
		break;
		case 0x11: // h, Volume: Slide down, No memory
		{
			if (value) Pattern_AddEffect_NoteVolume(pSong, cmd_down + flag_cmd_slide_frameN0, value << 2);
		}
		break;
		case 0x13: // j, Global: Jump
		{
			Pattern_AddGlbEffect_Position(pSong, gcmd_pos_jump, value);
		}
		break;
		case 0x15: // l, Global: Line jump
		{
			uint32_t ten = value >> 4;
			value &= 0xf;
			if ((value > 9) || (ten > 9))
				SysLog_BadEffectValue(pSong, 0, oeffect, ovalue);
			else
			{
				value += ten * 10;
				Pattern_AddGlbEffect_Position(pSong, gcmd_pos_linejump, value);
			}
		}
		break;
		case 0x1c: // s, Global: Set speed/tempo
		{
			convert_effect_MOD(pSong, channel, 0xf, value, note);
		}
		break;
		case 0x1f: // v, Volume: Set (Log)
		{
			Pattern_AddEffect_NoteVolume(pSong, cmd_set, value);
		}
		break;
		default:
		{
			SysLog_BadEffect(pSong, 0, oeffect, ovalue);
		}
	}
}

static const _kernel_oserror* Read_TagInfo(SongHdr* pSong, uint32_t* pTag, uint32_t* pTagSize)
{
	const _kernel_oserror* err = NULL;

	err = FileLoad_ReadInt(pSong, pTag, 4);
	if (err) return err;
	err = FileLoad_ReadInt(pSong, pTagSize, 4);

	return err;
}

const _kernel_oserror* Loader_MUSX(SongHdr* pSong)
{
	const _kernel_oserror* err = NULL;
	SubSong*        pSubSong = Song_GetSubSong(pSong, 0);
	uint8_t*        pLTrack = NULL;
	uint32_t        val;
	int             i, j, k;
	ChannelDefault* pDef;
	Pattern*        pPattern;
	Sample*         pSmp;
	uint8_t*        pc;
	uint32_t        FileSize, Tag, TagSize;
	uint32_t        FileOffset;
	const uint8_t*  SmpLogToLin = Table_ArcSampleLogToLin();

	// Allocate enough memory for a pattern
	err = Loaders_Alloc(pSong, (void**) &pLTrack, 8*256*4);
	if (err) goto loader_err;

	// is it a MUSX file?
	err = FileLoad_SetPos(pSong, 0);
	if (err) goto loader_err;
	err = Read_TagInfo(pSong, &Tag, &FileSize);
	if (err) goto loader_err;

	if (Tag != TAG(MUSX))
	{
		err = Loaders_NotThisType;
		goto loader_err;
	}
	FileSize += 8;

	pSong->Flags = 0;
	pSong->MinPitch = NoteDelta;
	pSong->MaxPitch = NoteDelta + 12 * 3 - 1;
	pSong->VolumeTranslation = Table_MUSXVolumeLogToLin();

	pSong->Samples = 36;

	// ... file type ptr
	pSong->pType = Typestr_MUSX;

	// Read TAGS
	while (FileLoad_GetPos(pSong) < FileSize)
	{
		// Read TAG id & size
		err = Read_TagInfo(pSong, &Tag, &TagSize);
		if (err) goto loader_err;
		FileOffset = FileLoad_GetPos(pSong);

		// Tag size is word aligned
		if (TagSize & 3)
		{
			err = Loaders_Error(pSong, FileOffset - 4, Invalid_Block);
			goto loader_err;
		}

		if (Tag == TAG(MVOX))
		{
			// Number of channels
			if (TagSize != 4)
			{
				err = Loaders_Error(pSong, FileOffset - 4, Invalid_Block);
				goto loader_err;
			}
			err = FileLoad_ReadInt(pSong, &pSong->Channels, 4);
			if (err) goto loader_err;
			if (!pSong->Channels
			|| (pSong->Channels > 8))
			{
				err = Loaders_InvalidSong(pSong, FileOffset - 4, &pSong->Channels, pSong->Channels);
				goto loader_err;
			}
		}
		else if (Tag == TAG(STER))
		{
			// Stereo positions
			if ((TagSize != 8)
			|| (!pSong->Channels))
			{
				err = Loaders_Error(pSong, FileOffset - 4, Invalid_Block);
				goto loader_err;
			}
			for (i = 0, pDef = pSubSong->Defaults.ChDef; i < pSong->Channels; i++, pDef++)
			{
				err = FileLoad_ReadByte(pSong, &val);
				if (err) goto loader_err;
				val &= 0x7;
				pDef->Panning = val << 5;
			}
		}
		else if (Tag == TAG(MNAM))
		{
			// Title
			err = FileLoad_ReadString(pSong, &pSong->pName, TagSize, false);
			if (err) goto loader_err;
		}
		else if (Tag == TAG(ANAM))
		{
			// Author
			err = FileLoad_ReadString(pSong, &pSong->pAuthor, TagSize, false);
			if (err) goto loader_err;
		}
		else if (Tag == TAG(MLEN))
		{
			// Nr of Orders
			if (TagSize != 4)
			{
				err = Loaders_Error(pSong, FileOffset - 4, Invalid_Block);
				goto loader_err;
			}
			err = FileLoad_ReadInt(pSong, &pSubSong->SeqLen, 4);
			if (err) goto loader_err;
			if (!pSubSong->SeqLen)
			{
				err = Loaders_InvalidSong(pSong, FileOffset - 4, &pSubSong->SeqLen, pSubSong->SeqLen);
				goto loader_err;
			}
		}
		else if (Tag == TAG(PNUM))
		{
			// Nr of Patterns
			if (TagSize != 4)
			{
				err = Loaders_Error(pSong, FileOffset - 4, Invalid_Block);
				goto loader_err;
			}
			err = FileLoad_ReadInt(pSong, &pSong->Patterns, 4);
			if (err) goto loader_err;
			if (!pSong->Patterns || (pSong->Patterns > 256)) // cf. sequence
			{
				err = Loaders_InvalidSong(pSong, FileOffset - 4, &pSong->Patterns, pSong->Patterns);
				goto loader_err;
			}
		}
		else if (Tag == TAG(PLEN))
		{
			// Nr of Rows/Pattern
			if (TagSize < pSong->Patterns)
			{
				err = Loaders_Error(pSong, FileOffset - 4, Invalid_Block);
				goto loader_err;
			}
			for (i = 0, pPattern = pSong->pPatterns; i < pSong->Patterns; i++, pPattern++)
			{
				err = FileLoad_ReadByte(pSong, &pPattern->Rows);
				if (err) goto loader_err;
				if (!pPattern->Rows)
				{
					err = Loaders_Error(pSong, FileOffset + i, "No rows for pattern %d", i);
				}
			}
		}
		else if (Tag == TAG(SEQU))
		{
			// Pattern Sequence
			if (TagSize < pSubSong->SeqLen)
			{
				err = Loaders_Error(pSong, FileOffset - 4, Invalid_Block);
				goto loader_err;
			}
			err = Loaders_AllocSeq(pSong, pSubSong);
			if (err) goto loader_err;

			for (i = 0; i < pSubSong->SeqLen; i++)
			{
				err = FileLoad_ReadByte(pSong, &val);
				pSubSong->pSeqs[i] = val;
				if (err) goto loader_err;
			}
		}
		else if (Tag == TAG(PATT))
		{
			// First pattern, rewind
			FileLoad_SetPos(pSong, FileOffset - 8);
			break;
		}

		// to prevent overflows
		if ((TagSize >= FileSize)
		||  (FileOffset + TagSize >= FileSize))
			break;

		FileLoad_SetPos(pSong, FileOffset + TagSize);
	}

	if (!pSong->Patterns
	||  !pSong->Channels
	||  !pSubSong->SeqLen
	||  !pSong->Samples)
	{
		err = Loaders_InvalidSong(pSong, 0, &pSong->pType, 0);
		goto loader_err;
	}

	// Read Patterns
	for (i = 0, pPattern = pSong->pPatterns; i < pSong->Patterns; i++, pPattern++)
	{
		uint32_t Note;

		// Read TAG id & size
		err = Read_TagInfo(pSong, &Tag, &TagSize);
		if (err) goto loader_err;
		FileOffset = FileLoad_GetPos(pSong);

		// PATT tag ?
		if (Tag != TAG(PATT))
		{
			err = Loaders_Error(pSong, FileOffset - 8, Invalid_Block);
			goto loader_err;
		}

		// Is Tag size correct ?
		if (TagSize != pSong->Channels*pPattern->Rows*4)
		{
			err = Loaders_Error(pSong, FileOffset - 4, Invalid_Block);
			goto loader_err;
		}

		// Read pattern data
		err = FileLoad_Read(pSong, pLTrack, TagSize);
		if (err) goto loader_err;

		err = Loaders_StartPattern(pSong, i);
		if (err) goto loader_err;

		for (j = 0, pc = pLTrack; j < pPattern->Rows; j++)
		{
			err = Loaders_StartRow(pSong, j);
			if (err) goto loader_err;

			for (k = 0; k  < pSong->Channels; k++, pc += 4)
			{
				Note = pc[3];
				if (Note)
				{
					if (Note <= 12*3)
						Note += NoteDelta - 1;
					else
					{
						SysLog_BadNote(pSong, 0, k, Note);
						Note = 0;
					}
				}
				err = Loaders_StartChannel(pSong, k, Note, pc[2]);
				if (err) goto loader_err;

				convert_effect(pSong, k, pc[1], pc[0], Note);

				err = Loaders_EndChannel(pSong);
				if (err) goto loader_err;
			}

			err = Loaders_EndRow(pSong);
			if (err) goto loader_err;
		}

		err = Loaders_EndPattern(pSong, pPattern);
		if (err) goto loader_err;

		FileLoad_SetPos(pSong, FileOffset + TagSize);
	}

	// Read Samples
	for (i = 0, pSmp = pSong->pSamples; i < pSong->Samples; i++, pSmp++)
	{
		int SmpEnd, RepeatLen = 0, RepeatOffset = 0;

		// Read TAG id & size
		err = Read_TagInfo(pSong, &Tag, &TagSize);
		if (err) goto smp_exit;
		FileOffset = FileLoad_GetPos(pSong);

		// SAMP tag ?
		if (Tag != TAG(SAMP))
		{
			SysLog_FileCorrupted(SysLog_High, pSong, FileOffset - 8, Invalid_Block);
			goto smp_exit;
		}

		// Is Tag word aligned ?
		if (TagSize & 3)
		{
			SysLog_FileCorrupted(SysLog_High, pSong, FileOffset - 4, Invalid_Block);
			goto smp_exit;
		}
		SmpEnd = FileOffset + TagSize;

		while (FileLoad_GetPos(pSong) < SmpEnd)
		{
			// Read TAG id & size
			err = Read_TagInfo(pSong, &Tag, &TagSize);
			if (err) goto smp_err;
			FileOffset = FileLoad_GetPos(pSong);

			if (Tag == TAG(SNAM))
			{
				// Is Tag word aligned ?
				if (TagSize & 3)
				{
					SysLog_FileCorrupted(SysLog_High, pSong, FileOffset - 4, Invalid_Block);
					goto smp_err;
				}
				// Name
				err = FileLoad_ReadString(pSong, &pSmp->pName, TagSize, false);
				if (err) goto loader_err;
			}
			else if (Tag == TAG(SVOL))
			{
				if (TagSize != 4)
				{
					SysLog_FileCorrupted(SysLog_High, pSong, FileOffset - 4, Invalid_Block);
					goto smp_err;
				}
				err = FileLoad_Read(pSong, &val, 4);
				if (err) goto smp_err;
				if (val <= 256) pSmp->DefaultVolume = val;
			}
			else if (Tag == TAG(SLEN))
			{
				if (TagSize != 4)
				{
					SysLog_FileCorrupted(SysLog_High, pSong, FileOffset - 4, Invalid_Block);
					goto smp_err;
				}
				err = FileLoad_Read(pSong, &pSmp->Size, 4);
				if (err) goto smp_err;
			}
			else if (Tag == TAG(ROFS))
			{
				if (TagSize != 4)
				{
					SysLog_FileCorrupted(SysLog_High, pSong, FileOffset - 4, Invalid_Block);
					goto smp_err;
				}
				err = FileLoad_Read(pSong, &RepeatOffset, 4);
				if (err) goto smp_err;
			}
			else if (Tag == TAG(RLEN))
			{
				if (TagSize != 4)
				{
					SysLog_FileCorrupted(SysLog_High, pSong, FileOffset - 4, Invalid_Block);
					goto smp_err;
				}
				err = FileLoad_Read(pSong, &RepeatLen, 4);
				if (err) goto smp_err;
			}
			else if (Tag == TAG(SDAT))
			{
				if (TagSize < pSmp->Size)
				{
					SysLog_FileCorrupted(SysLog_High, pSong, FileOffset - 4, Invalid_Block);
					goto smp_err;
				}
				err = FileLoad_ReadSample(pSong, pSmp);
				if (err) goto loader_err;
				// Convert from Log to Lin
				for (j = 0, pc = pSmp->Ptr; j < pSmp->Size; j++, pc++)
				{
					*pc = SmpLogToLin[*pc];
				}
				// Is Tag word aligned? Just warn about it, after loading the data
				if (TagSize & 3)
				{
					SysLog_FileCorrupted(SysLog_High, pSong, FileOffset - 4, Invalid_Block);
					goto smp_err;
				}

			}

			FileLoad_SetPos(pSong, FileOffset + TagSize);
		}

smp_err:
		// Continue with next sample if sample tags not correct
		if (err)
		{
			SysLog_FileCorrupted(SysLog_High, pSong, 0, "%s", &err->errmess[0]);
			err = NULL;
		}

		if (RepeatLen == 2)
		{
			if (RepeatOffset)
				RepeatLen = pSmp->Size - RepeatOffset;
			else
				RepeatLen = 0;
		}

		// due to a bad MOD -> MUSX conversion tool
		// some trackers have a loop start non divided by 2
		if (RepeatOffset + RepeatLen > pSmp->Size);
			RepeatOffset >>= 1;

		if (RepeatLen)
		{
			pSmp->Type |= Smp_Type_Loop;
			pSmp->LoopStart = RepeatOffset;
			pSmp->LoopEnd = RepeatOffset + RepeatLen;
		}

		FileLoad_SetPos(pSong, SmpEnd);
	}

smp_exit:
	// Accept files truncated within the samples
	if (err)
	{
		SysLog_FileCorrupted(SysLog_High, pSong, 0, "%s", &err->errmess[0]);
		err = NULL;
	}

loader_err:
	Loaders_Free(pSong, pLTrack);

	return err;
}
