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

#include "swis.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"

#define DSym_Load			0x45880
#define DSym_RestartSong	0x45881
#define DSym_Vol			0x45882
#define DSym_ResetParams	0x45887
#define DSym_FreezeOutput	0x45889
#define DSym_SongPos		0x4588C
#define DSym_TuneControl	0x4588D
#define DSym_Loop			0x45894
#define DSym_SampleRate		0x45897
#define DSym_TuneInfo		0x4589A
#define DSym_LoopType		0x4589F

extern const MediaDriver DSDriver;

static const char dsmodname[] = "DSymphonyPlayer";

typedef struct DSDriver_ObjectData
{
	unsigned int	voices;
} DSDriver_ObjectData;

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

static const char DSDriver_Name[] = "Digital Symphony";

static MediaObject* inUse = NULL;

static bool throw_DSDriver_LoadObject(MediaObject* pobj, const char* pObjectName)
{
	DSDriver_ObjectData* volatile	pData = NULL;
	unsigned int i, len;
	char title[256];
	FILE* file;

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

	pData = throw_mem_alloc(sizeof(DSDriver_ObjectData));

	Media_Attach(pobj, pData, Media_HType_File);

	// Ensure DSym module is loaded
	throw_os(RMEnsure(dsmodname, 146, "System:Modules.Audio.Trackers.DSymPlay"));

	// stop playing and load a new file
	_swix(DSym_TuneControl, 0);

	throw_os(_swix(DSym_Load, _IN(0)|_IN(2)|_IN(4), pObjectName, 0, 0));

	inUse = pobj;

	// extract title info
	if ((file = fopen(pObjectName, "rb")) != NULL)
	{
		try
		{
			// extract voices in header
			fseek(file, 9, SEEK_SET);
			pData->voices = fgetc(file);

			// must skip header and instruments info
			fseek(file, 17, SEEK_SET);
			for(i = 0; i < 63; i++)
			{
				if (fgetc(file) < 128)
				{
					fgetc(file);fgetc(file);fgetc(file);
				}
			}
			// read title
			len = fgetc(file);
			fread(title, len, 1, file);
			title[len] = 0;
			// strip leading and trailing blanks
			String_StripBlanks(title);
			fclose(file);
		}
		catch
		{
			fclose(file);
			throw_current();
		}
		catch_end
	}
	else title[0] = 0;

	if (title[0] > 32)
		throw_MediaObject_SetText(pobj, EMetaId_StreamTitle, EMetaOrigin_Stream, title);

	throw_MediaObject_SetText(pobj, EMetaId_StreamType, EMetaOrigin_Stream
		, SPrintf("%s - %d Ch", DSDriver_Name, pData->voices));
	throw_MediaObject_SetText(pobj, EMetaId_ModuleName, EMetaOrigin_Stream, dsmodname);
	throw_MediaObject_SetMeta(pobj, EMetaId_ModuleHandle, EMetaOrigin_Stream, (void*) 0, 0);

	// start playing track
	_swix(DSym_LoopType, _IN(0), 1);
	_swix(DSym_FreezeOutput, _IN(0), 1);

	return true;
}

static void DSDriver_UnLoadObject(MediaObject* pobj)
{
	DSDriver_ObjectData* pData = pobj->m_pData;

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

	if (inUse == pobj)
	{
		inUse = NULL;
		_swix(DSym_TuneControl, 0);
		_swix(DSym_SongPos, _IN(0)|_IN(1), 0, 0);
		_swix(DSym_ResetParams, 0);
		_swix(DSym_Load, _IN(0), -1);
		if (Options()->Music_Files.bUnloadModules)
			RMKill(dsmodname);
	}
}

static void DSDriver_Play(MediaObject* pobj, uint64_t currentpos)
{
	unsigned int pos = ((unsigned int) currentpos) >> 6;

	// if pos = 0 song is either started or restarted
	if (pos == 0)
	{
		_swix(DSym_RestartSong, 0);
		_swix(DSym_SampleRate, _IN(0), pobj->m_iSpeed);

	}
	_swix(DSym_SongPos, _IN(0)|_IN(1), pos, 0);
}

static void DSDriver_Freeze(MediaObject* pobj, bool on)
{
	IGNORE(pobj);

	_swix(DSym_FreezeOutput, _IN(0), !on);
}

static void DSDriver_Stop(MediaObject* pobj)
{
	IGNORE(pobj);

	_swix(DSym_TuneControl, 0);
	_swix(DSym_SongPos, _IN(0)|_IN(1), 0, 0);
}

static void DSDriver_Seek(MediaObject* pobj, EMedia_Seek set)
{
	unsigned int pos = 0;

	if (_swix(DSym_SongPos, _IN(0)|_IN(1)|_OUT(0), -1, -1, &pos))
		pos = 0;
	pos <<= 6;

	switch(set)
	{
		case EMedia_Seek_Start:
		case EMedia_Seek_SectionStart:
		{
			DSDriver_Play(pobj, 0);
		}
		break;
		case EMedia_Seek_PreviousPos:
		{
			if (pos < 64)
				pos = 0;
			else
				pos -= 64;

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

			_swix(DSym_TuneInfo, _OUT(0), &length);
			length <<= 6;

			pos += 64;
			if (pos < length)
				DSDriver_Play(pobj, (uint64_t) pos);
		}
		break;
		default: ;
	}
}

static void DSDriver_SetLoopMode(MediaObject* pobj, Media_LoopMode mode)
{
	pobj->m_LoopMode = mode;
	_swix( DSym_Loop, _IN(0)|_IN(1)
		 , (pobj->m_LoopMode != loop_section_loop) && (pobj->m_LoopMode != loop_volume_loop)
		 , 0);
}

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

static Media_Status DSDriver_GetStatus(MediaObject* pobj)
{
	unsigned int status, status2;
	const _kernel_oserror* e;

	IGNORE(pobj);

	e = _swix(DSym_TuneControl, _IN(0)|_OUT(0), -1, &status);
	if (e) goto error;

	if (status == 0) return status_stop;

	e = _swix(DSym_FreezeOutput, _IN(0)|_OUT(0), -1, &status);
	if (e) goto error;

	if (status == 0) return status_pause;

	e = _swix(DSym_Loop, _IN(0)|_IN(1)|_OUT(0)|_OUT(1), -1, -1, &status, &status2);
	if (e) goto error;

	if (status2 != 0) _swix(DSym_Loop, _IN(0)|_IN(1), -1, 0);
	if ((status != 0) && (status2 != 0)) return status_stop;

	return status_play;

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

	return pobj->m_Status;
}

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

	IGNORE(pobj);

	if (_swix(DSym_SongPos, _IN(0)|_IN(1)|_OUT(0)|_OUT(1), -1, -1, &pos1, &pos2))
		pos1 = 0;
	else
		pos1 = (pos1 << 6) + pos2;
	_swix(DSym_TuneInfo, _OUT(0), &len);
	len <<= 6;

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

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

			// Notes: DSym takes into account system volume
			//        when loading file (changes while playing are ignored)
			if (val > 0x10000) val = 0x10000;
			_swix(DSym_Vol, _IN(0), val >> 10);
		}
		break;
		case Media_CfgFrequency:
		{
			if (_swix(DSym_SampleRate, _IN(0), Quality_FromFrequency(*(unsigned int*) value))) return;

			_swix(DSym_SampleRate, _IN(0)|_OUT(0), -1, &pobj->m_iSpeed);
		}
		break;
		default: ;
	}
}

static unsigned int DSDriver_GetConfig(MediaObject* pobj, Media_Config type)
{
	IGNORE(pobj);

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

			if (_swix(DSym_Vol, _IN(0)|_OUT(0), -1, &volume)) return 0xffff;

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

	return 0xffffffff;
}

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

const MediaDriver DSDriver =
{
	  DSDriver_Name
	, throw_DSDriver_LoadObject
	, DSDriver_UnLoadObject
	, NULL
	, DSDriver_Play
	, DSDriver_Freeze
	, DSDriver_Stop
	, DSDriver_Seek
	, DSDriver_SetLoopMode
	, DSDriver_GetLoopMode
	, DSDriver_GetStatus
	, DSDriver_GetPosition
	, NULL
	, DSDriver_SetConfig
	, DSDriver_GetConfig
};
