#include <stdio.h>
#include <string.h>

#include "WimpLib:Exception.h"
#include "Media.h"
#include "MediaDrv.h"
#include "Metadata.h"
#include "WimpLib:mem.h"
#include "Options.h"
#include "WimpLib:Utils.h"

extern const MediaDriver TRKDriver;

typedef struct TRKDriver_ObjectData
{
	unsigned int	lastpos;
} TRKDriver_ObjectData;

//------------------------------------------------------------------------------

static const _kernel_oserror TRK_Err = {1, "Matrix Load Error"};

static const _kernel_oserror* TRK_Load(const char* file)
{
	_kernel_swi_regs regs;
	const _kernel_oserror* e;

	regs.r[0] = 0;
	// matrix_TRKRelease
	_kernel_swi(0x06bd54, &regs, &regs);

	regs.r[0] = (int) file;
	regs.r[1] = 0;
	regs.r[2] = 0;
	regs.r[3] = 0;
	regs.r[4] = 0;
	regs.r[5] = 0;
	regs.r[6] = 0;
	regs.r[7] = 0;
	regs.r[8] = 0;
	// matrix_TRKValidity
	if ((e = _kernel_swi(0x06bd57, &regs, &regs)) != NULL) return e;

	if (regs.r[1])
		return &TRK_Err;

	regs.r[1] = 0;
	regs.r[2] = 0;
	regs.r[3] = 0;
	regs.r[4] = 0;
	regs.r[5] = 0;
	regs.r[6] = 0;
	regs.r[7] = 0;
	regs.r[8] = 0;
	// matrix_TRKLoad
	e = _kernel_swi(0x06bd51, &regs, &regs);
	return e;
}

static const _kernel_oserror* TRK_Start(void)
{
	_kernel_swi_regs regs;
	const _kernel_oserror* e;

	regs.r[0] = 1;
	regs.r[1] = 0;
	regs.r[2] = 0;
	regs.r[3] = 0;
	regs.r[4] = 0;
	regs.r[5] = 0;
	regs.r[6] = 0;
	regs.r[7] = 0;
	regs.r[8] = 0;
	// Xmatrix_TRKLink
	e = _kernel_swi(0x06bd52, &regs, &regs);
	return e;
}

static const _kernel_oserror* TRK_Stop(void)
{
	_kernel_swi_regs regs;

	regs.r[0] = 0;
	regs.r[1] = 0;
	regs.r[2] = 0;
	regs.r[3] = 0;
	regs.r[4] = 0;
	regs.r[5] = 0;
	regs.r[6] = 0;
	regs.r[7] = 0;
	regs.r[8] = 0;
	// Xmatrix_TRKUnLink
	return _kernel_swi(0x06bd53, &regs, &regs);
}

static const _kernel_oserror* TRK_Pause(void)
{
	_kernel_swi_regs regs;

	regs.r[0] = 0;
	regs.r[1] = 0;
	regs.r[2] = 0;
	regs.r[3] = 0;
	regs.r[4] = 0;
	regs.r[5] = 0;
	regs.r[6] = 0;
	regs.r[7] = 0;
	regs.r[8] = 0;
	// Xmatrix_TRKPause
	return _kernel_swi(0x06bd5a, &regs, &regs);
}

static const _kernel_oserror* TRK_UnLoad(void)
{
	_kernel_swi_regs regs;

	regs.r[0] = 0;
	regs.r[1] = 0;
	regs.r[2] = 0;
	regs.r[3] = 0;
	regs.r[4] = 0;
	regs.r[5] = 0;
	regs.r[6] = 0;
	regs.r[7] = 0;
	regs.r[8] = 0;
	// matrix_TRKRelease
	return _kernel_swi(0x06bd54, &regs, &regs);
}

static const _kernel_oserror* TRK_SongPos(unsigned int setpos, unsigned int setevent, unsigned int* ppos)
{
	_kernel_swi_regs regs;
	const _kernel_oserror* e;

	regs.r[0] = (setpos == -1) ? 0 : 2;
	regs.r[1] = setpos;
	regs.r[2] = setevent;
	regs.r[3] = 0;
	regs.r[4] = 0;
	regs.r[5] = 0;
	regs.r[6] = 0;
	regs.r[7] = 0;
	regs.r[8] = 0;
	// matrix_TRKControlSequence
	e = _kernel_swi(0x06bd59, &regs, &regs);
	if (e) return e;

	*ppos = (unsigned int) regs.r[0];

	return NULL;
}

static const _kernel_oserror* TRK_Vol(unsigned int set, unsigned int* read)
{
	_kernel_swi_regs regs;
	const _kernel_oserror* e;

	regs.r[0] = set;
	regs.r[1] = 0;
	regs.r[2] = 0;
	regs.r[3] = 0;
	regs.r[4] = 0;
	regs.r[5] = 0;
	regs.r[6] = 0;
	regs.r[7] = 0;
	regs.r[8] = 0;
	// Xmatrix_TRKVolume
	if ((e = _kernel_swi(0x06bd56, &regs, &regs)) != NULL) return e;

	*read = regs.r[0];

	return NULL;
}

static const _kernel_oserror* TRK_SongLength(unsigned int* len)
{
	_kernel_swi_regs regs;
	const _kernel_oserror* e;

	regs.r[0] = 0;
	regs.r[1] = 0;
	regs.r[2] = 0;
	regs.r[3] = 0;
	regs.r[4] = 0;
	regs.r[5] = 0;
	regs.r[6] = 0;
	regs.r[7] = 0;
	regs.r[8] = 0;
	// Xmatrix_TRKControlSequence
	if ((e = _kernel_swi(0x06bd59, &regs, &regs)) != NULL) return e;

	*len = regs.r[1];

	return NULL;
}

static const _kernel_oserror* TRK_SampleRate(unsigned int set, unsigned int* read)
{
	_kernel_swi_regs regs;
	const _kernel_oserror* e;

	regs.r[0] = set;
	regs.r[1] = 0;
	regs.r[2] = 0;
	regs.r[3] = 0;
	regs.r[4] = 0;
	regs.r[5] = 0;
	regs.r[6] = 0;
	regs.r[7] = 0;
	regs.r[8] = 0;
	// Xmatrix_TRKSamplePeriod
	if ((e = _kernel_swi(0x06bd55, &regs, &regs)) != NULL) return e;

	regs.r[0] = 0;
	if ((e = _kernel_swi(0x06bd55, &regs, &regs)) != NULL) return e;

	*read = regs.r[0];

	return NULL;
}

//------------------------------------------------------------------------------

static const char TRKDriver_Name[] = "MatrixTRK";

static MediaObject* inUse = NULL;

static bool throw_TRKDriver_LoadObject(MediaObject* pobj, const char* pObjectName)
{
	TRKDriver_ObjectData* volatile	pData = NULL;
	static char			string[1024];
	FILE*				file = NULL;
	unsigned int			voices = 0;
	unsigned int			len;
	_kernel_swi_regs	regs;
	const char		module[] = "matrixTRK";

	// Module allows only a single soundtrack file at any time
	if (inUse) throw_os(&Media_ErrorModuleInUse);

	pData = throw_mem_alloc(sizeof(TRKDriver_ObjectData));

	Media_Attach(pobj, pData, Media_HType_File);

	// Ensure TRK modules are loaded
	throw_os(RMEnsure("matrixLink", 53, "System:Modules.Audio.Trackers.matrixLink"));
	throw_os(RMEnsure("matrixSQSH", 17, "System:Modules.Audio.Trackers.matrixSQSH"));
	throw_os(RMEnsure("matrixADPCM", 21, "System:Modules.Audio.Trackers.mtrxADPCM"));
	throw_os(RMEnsure("matrixTRK", 89, "System:Modules.Audio.Trackers.matrixTRK"));

	// stop playing and load a new file
	TRK_Stop();
	throw_os(TRK_Load(pObjectName));
	inUse = pobj;

	// extract title info
	if ((file = fopen(pObjectName, "rb")) != NULL)
	{
		// must skip header and instruments info
		fseek(file, 14, SEEK_SET);
		voices = fgetc(file);
		voices += fgetc(file) << 8;
		fseek(file, 28 + (long int) voices * 4, SEEK_SET);
		// read title
		len = fgetc(file);
		len += fgetc(file) << 8;
		len += fgetc(file) << 16;
		len += fgetc(file) << 24;
		fread(string, len, 1, file);
		string[len] = 0;
		fclose(file);

		if (strlen(string) > 0)
			throw_MediaObject_SetText(pobj, EMetaId_StreamTitle, EMetaOrigin_Stream, string);
	}

	regs.r[0] = 18;
	regs.r[1] = (int) module;
	_kernel_swi(0x02001e, &regs, &regs);

	throw_MediaObject_SetText(pobj, EMetaId_StreamType, EMetaOrigin_Stream
		, SPrintf("%s - %d Ch", TRKDriver_Name, ((unsigned int*) regs.r[4])[13]));
	throw_MediaObject_SetText(pobj, EMetaId_ModuleName, EMetaOrigin_Stream, "matrixTRK");
	throw_MediaObject_SetMeta(pobj, EMetaId_ModuleHandle, EMetaOrigin_Stream, (void*) 0, 0);

	return true;
}

static void TRKDriver_UnLoadObject(MediaObject* pobj)
{
	TRKDriver_ObjectData* pData = pobj->m_pData;

	if (pData)
	{
		mem_free(pData);
		pobj->m_pData = NULL;
	}

	if (inUse == pobj)
	{
		inUse = NULL;
		TRK_Stop();
		TRK_UnLoad();
		if (Options()->Music_Files.bUnloadModules)
		{
			RMKill("matrixTRK");
			RMKill("matrixSQSH");
			RMKill("mtrxADPCM");
			RMKill("matrixLink");
		}
	}
}

static void TRKDriver_Play(MediaObject* pobj, uint64_t currentpos)
{
	TRKDriver_ObjectData* pData = pobj->m_pData;
	unsigned int pos = (unsigned int) currentpos;
	unsigned int dummy;

	// if pos = 0, song is either started or restarted
	if (pos == 0)
	{
		TRK_Stop();
		TRK_SampleRate(pobj->m_iSpeed, &dummy);
	}
	pobj->m_Status = status_play;
	TRK_Start();
	TRK_SongPos(pos, 0, &dummy);
	TRK_SongPos(-1, -1, &pData->lastpos);
}

static void TRKDriver_Freeze(MediaObject* pobj, bool on)
{
	if (on)
	{
		TRK_Pause();
		pobj->m_Status = status_pause;
	}
	else
	{
		TRK_Start();
		pobj->m_Status = status_play;
	}
}

static void TRKDriver_Stop(MediaObject* pobj)
{
	TRK_Stop();
	pobj->m_Status = status_stop;
}

static void TRKDriver_Seek(MediaObject* pobj, EMedia_Seek set)
{
	switch(set)
	{
		case EMedia_Seek_Start:
		case EMedia_Seek_SectionStart:
		{
			TRKDriver_Play(pobj, 0);
		}
		break;
		case EMedia_Seek_PreviousPos:
		{
			unsigned int pos;
			TRK_SongPos(-1, -1, &pos);

			if (pos < 1)
				pos = 0;
			else
				pos -= 1;

			TRKDriver_Play(pobj, (uint64_t) pos);
		}
		break;
		case EMedia_Seek_NextPos:
		{
			unsigned int pos;
			unsigned int length;

			TRK_SongPos(-1, -1, &pos);
			pos += 1;
			TRK_SongLength(&length);
			if (pos < length)
				TRKDriver_Play(pobj, (uint64_t) pos);
		}
		break;
		default: ;
	}
}

static void TRKDriver_SetLoopMode(MediaObject* pobj, Media_LoopMode mode)
{
	pobj->m_LoopMode = mode;
}

static Media_LoopMode TRKDriver_GetLoopMode(MediaObject* pobj)
{
	return pobj->m_LoopMode;
}

static Media_Status TRKDriver_GetStatus(MediaObject* pobj)
{
	TRKDriver_ObjectData* pData = pobj->m_pData;
	const _kernel_oserror* e;
	unsigned int pos;

	if (pobj->m_Status == status_play)
	{
		e = TRK_SongPos(-1, -1, &pos);
		if (e) goto error;

		if ((pobj->m_LoopMode != loop_section_loop)
		&&  (pobj->m_LoopMode != loop_volume_loop)
		&&  (pData->lastpos > pos))
		{
			TRKDriver_Stop(pobj);
			return status_stop;
		}
		pData->lastpos = pos;
		return status_play;
	}

	// stop or pause ?
	return pobj->m_Status;

error:
	pobj->m_Err = *e;
	pobj->m_Status = status_error;

	return pobj->m_Status;
}

static void TRKDriver_GetPosition(MediaObject* pobj, MediaPosition* pos)
{
	unsigned int pos1 = 0;
	unsigned int len;

	// Stop warnings
	IGNORE(pobj);

	TRK_SongPos(-1, -1, &pos1);
	TRK_SongLength(&len);

	pos->driver.start = 0;
	pos->driver.pos = (uint64_t) pos1;
	pos->driver.end = (uint64_t) len;
	pos->volume.sections = 1;
	pos->volume.pos = pos1 * 640;
	pos->volume.len = len * 640;
	pos->section.pos = pos->volume.pos;
	pos->section.len = pos->volume.len;
	pos->section.nr = 0;
}

static void TRKDriver_SetConfig(MediaObject* pobj, Media_Config type, const void* value)
{
	switch(type)
	{
		case Media_CfgVolume:
		{
			MediaVolume* pvol = (MediaVolume*) value;
			unsigned int val = pvol->sysscaled;
			unsigned int dummy;

			if (val > 0x10000) val = 0x10000;
			TRK_Vol(val >> 10, &dummy);
		}
		break;
		case Media_CfgFrequency:
		{
			// change not supported during playback
			pobj->m_iSpeed = Quality_FromFrequency(*(unsigned int*) value);
		}
		break;
		default: ;
	}
}

static unsigned int TRKDriver_GetConfig(MediaObject* pobj, Media_Config type)
{
	// Stop warnings
	IGNORE(pobj);

	switch(type)
	{
		case Media_CfgVolume:
		{
			unsigned int volume;

			if (TRK_Vol(-1, &volume)) return 0xffff;

			return volume << 10;
		}
		break;
		default: ;
	}

	return 0xffffffff;
}

//------------------------------------------------------------------------------

const MediaDriver TRKDriver =
{
	  TRKDriver_Name
	, throw_TRKDriver_LoadObject
	, TRKDriver_UnLoadObject
	, NULL
	, TRKDriver_Play
	, TRKDriver_Freeze
	, TRKDriver_Stop
	, TRKDriver_Seek
	, TRKDriver_SetLoopMode
	, TRKDriver_GetLoopMode
	, TRKDriver_GetStatus
	, TRKDriver_GetPosition
	, NULL
	, TRKDriver_SetConfig
	, TRKDriver_GetConfig
};
