#include <string.h>

#include "swis.h"

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

void Equalizer_Configure(int swi, EqualizerParams* pParams);
static Media_Status FileDriver_GetStatus(MediaObject* pobj);

extern const MediaDriver FileDriver;
extern const MediaDriver FileExtDriver;
extern const MediaDriver UrlDriver;

typedef struct FileDriver_ObjectData
{
	unsigned int	handle;
	bool			isReady;
	uint64_t		curtime;
	uint64_t		songtime;
} FileDriver_ObjectData;

static FileDriver_ObjectData* throw_ObjectData_New(void)
{
	FileDriver_ObjectData* pData = throw_mem_alloc(sizeof(FileDriver_ObjectData));

	pData->handle = -1;
	pData->isReady = false;
	pData->curtime = 0;
	pData->songtime = 0;

	return pData;
}

static void ObjectData_Delete(FileDriver_ObjectData* pData)
{
	mem_free(pData);
}

#define DiskSample_Version			0x52ec0
#define DiskSample_Configure		0x52ec1
#define DiskSample_FileOpen			0x52ec2
#define DiskSample_StreamClose		0x52ec3
#define DiskSample_StreamCreate		0x52ec4
#define DiskSample_StreamSource		0x52ec5
#define DiskSample_StreamPlay		0x52ec8
#define DiskSample_StreamPause		0x52ec9
#define DiskSample_StreamStop		0x52eca
#define DiskSample_StreamPosition	0x52ecb
#define DiskSample_StreamVolume		0x52ecc
#define DiskSample_StreamIsReady	0x52ecd
#define DiskSample_StreamStatus		0x52ece
#define DiskSample_StreamChain		0x52ecf
#define DiskSample_StreamInfo		0x52ed0
#define DiskSample_StreamTexts		0x52ed1
#define DiskSample_StreamParam		0x52ed2
#define DiskSample_StreamDecoding	0x52ed3

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

static void FileDriver_SetConfig(MediaObject* pobj, Media_Config type, const void* value);

static const char FileDriver_Name[] = "DiskSample";
static const char FileExtDriver_Name[] = "DiskSample (raw)";

static FileDriver_ObjectData* throw_Driver_Prepare(MediaObject* pobj)
{
	FileDriver_ObjectData* volatile	pData = NULL;

	pData = throw_ObjectData_New();

	Media_Attach(pobj, pData, Media_HType_File);

	// Ensure modules are loaded
	throw_os(RMEnsure(FileDriver_Name, 51, "System:Modules.Audio.DiskSample"));

	_swix(DiskSample_Configure, _INR(0,1),   8, Options()->Drivers.DiskSample.bFreeVolume);
	_swix(DiskSample_Configure, _INR(0,1),  10, Options()->Drivers.DiskSample.bEnableInterrupts);
	_swix(DiskSample_Configure, _INR(0,1), 256, Options()->Drivers.DiskSample.InputBufferSize);
	_swix(DiskSample_Configure, _INR(0,1), 257, Options()->Drivers.DiskSample.OutputBufferSize);
	_swix(DiskSample_Configure, _INR(0,1), 258, Options()->Internet.bIcyMetadata);
	_swix(DiskSample_Configure, _INR(0,2), 259, Options()->Internet.bAudiocastMetadata, Options()->Internet.AudiocastPort);
	_swix(DiskSample_Configure, _INR(0,1), 260, Options()->Internet.Reserve);

	return pData;
}

static void FileDriver_UnLoadObject(MediaObject* pobj)
{
	FileDriver_ObjectData* pData = pobj->m_pData;

	if (pData)
	{
		if (pData->handle != -1) _swix(DiskSample_StreamClose, _IN(0), pData->handle);
		ObjectData_Delete(pData);
		pobj->m_pData = NULL;

		if (Options()->Music_Files.bUnloadModules)
		{
			RMKill("DiskSample");
			RMKill("AudioMPEG");
			RMKill("Vorbis");
		}
	}
}

static bool throw_UrlDriver_LoadObject(MediaObject* pobj, const char* pObjectName)
{
	FileDriver_ObjectData* volatile	pData = NULL;

	pData = throw_Driver_Prepare(pobj);
	Media_SetSeekable(pobj, false);

	// load a new url
	throw_os(_swix(DiskSample_StreamCreate, _IN(1)|_OUT(0)
		, Options()->Drivers.DiskSample.InputBufferSize
		, &pData->handle));
	throw_os(_swix(DiskSample_StreamSource, _INR(0,3), pData->handle, 2
		, pObjectName
		, (Options()->Internet.bUseProxy && *Options()->Internet.ProxyUrl) ? Options()->Internet.ProxyUrl : NULL));
	throw_MediaObject_SetText(pobj, EMetaId_ModuleName, EMetaOrigin_Stream, FileDriver_Name);
	throw_MediaObject_SetMeta(pobj, EMetaId_ModuleHandle, EMetaOrigin_Stream, (void*) pData->handle, 0);

	return true;
}

static bool throw_FileExtDriver_LoadObject(MediaObject* pobj, const char* pObjectName)
{
	FileDriver_ObjectData* volatile	pData = NULL;
	const char* ext = strrchr(pObjectName, FILE_EXTENSION_CHAR);
	struct
	{
		int size;
		const char* codecname;
		int flags;
	} desc = {0, NULL, 1};

	if (!ext) throw_string(SPrintf(Msg_Lookup("ErrNoExt"), pObjectName));
	desc.codecname = ext + 1;

	pData = throw_Driver_Prepare(pobj);

	// load a new file
	throw_os(_swix(DiskSample_StreamCreate, _IN(1)|_OUT(0)
		, Options()->Drivers.DiskSample.InputBufferSize
		, &pData->handle));
	throw_os(_swix(DiskSample_StreamSource, _INR(0,3), pData->handle, 3
		, pObjectName
		, &desc));
	throw_MediaObject_SetText(pobj, EMetaId_ModuleName, EMetaOrigin_Stream, FileDriver_Name);
	throw_MediaObject_SetMeta(pobj, EMetaId_ModuleHandle, EMetaOrigin_Stream, (void*) pData->handle, 0);

	return true;
}

static bool throw_FileDriver_LoadObject(MediaObject* pobj, const char* pObjectName)
{
	FileDriver_ObjectData* volatile	pData = NULL;

	pData = throw_Driver_Prepare(pobj);

	// load a new file
	throw_os(_swix(DiskSample_FileOpen, _IN(1)|_OUT(0), pObjectName, &pData->handle));
	throw_MediaObject_SetText(pobj, EMetaId_ModuleName, EMetaOrigin_Stream, FileDriver_Name);
	throw_MediaObject_SetMeta(pobj, EMetaId_ModuleHandle, EMetaOrigin_Stream, (void*) pData->handle, 0);

	return true;
}

static void FileDriver_Chain(MediaObject* pobj, MediaObject* pnext)
{
	if (pobj->m_pDriver == pnext->m_pDriver)
	{
		FileDriver_ObjectData* pData1 = pobj->m_pData;
		FileDriver_ObjectData* pData2 = pnext->m_pData;
		_swix(DiskSample_StreamChain, _INR(0,1), pData1->handle, pData2->handle);
	}
}

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

	// if pos = 0, song is either started or restarted
	if (pos == 0)
	{
		_swix(DiskSample_StreamPause, _IN(0), pData->handle);
		FileDriver_SetConfig(pobj, Media_CfgFrequency, &pobj->m_iSpeed);
		FileDriver_SetConfig(pobj, Media_CfgDMASize, &Options()->Music_Files.DMASize);
	}
	if (pobj->m_Status == status_play)
	{
//		_swix(DiskSample_StreamPosition, _IN(0)|_IN(1), pData->handle, pos);
		_swix(DiskSample_StreamPlay, _IN(0), pData->handle);
	} // else delayed till stream is ready
	pobj->m_Status = status_play;
}

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

	// if pos = 0, song is either started or restarted
	if (pos == 0)
	{
		_swix(DiskSample_StreamPause, _IN(0), pData->handle);
		FileDriver_SetConfig(pobj, Media_CfgFrequency, &pobj->m_iSpeed);
		FileDriver_SetConfig(pobj, Media_CfgDMASize, &Options()->Music_Files.DMASize);
	}
	if (pobj->m_Status == status_play)
	{
		_swix(DiskSample_StreamPosition, _IN(0)|_IN(1), pData->handle, pos);
		_swix(DiskSample_StreamPlay, _IN(0), pData->handle);
	}
	else
	{
		// delayed till stream is ready
		pobj->m_Status = status_play;
		FileDriver_GetStatus(pobj);
	}
}

static void UrlDriver_Freeze(MediaObject* pobj, bool on)
{
	// do nothing
	IGNORE(pobj);
	IGNORE(on);
}

static void FileDriver_Freeze(MediaObject* pobj, bool on)
{
	FileDriver_ObjectData* pData = pobj->m_pData;

	if (on)
	{
		_swix(DiskSample_StreamPause, _IN(0), pData->handle);
		pobj->m_Status = status_pause;
	}
	else
	{
		_swix(DiskSample_StreamPlay, _IN(0), pData->handle);
		pobj->m_Status = status_play;
		FileDriver_SetConfig(pobj, Media_CfgFrequency, &pobj->m_iSpeed);
		FileDriver_SetConfig(pobj, Media_CfgDMASize, &Options()->Music_Files.DMASize);
	}
}

static void FileDriver_Stop(MediaObject* pobj)
{
	FileDriver_ObjectData* pData = pobj->m_pData;

	_swix(DiskSample_StreamStop, _IN(0), pData->handle);
	pobj->m_Status = status_stop;
}

static void UrlDriver_Seek(MediaObject* pobj, EMedia_Seek set)
{
	// do nothing
	IGNORE(pobj);
	IGNORE(set);
}

static void FileDriver_Seek(MediaObject* pobj, EMedia_Seek set)
{
	FileDriver_ObjectData* pData = pobj->m_pData;

	switch(set)
	{
		case EMedia_Seek_Start:
		case EMedia_Seek_SectionStart:
		{
			FileDriver_Play(pobj, 0);
		}
		break;
		case EMedia_Seek_PreviousPos:
		{
			uint64_t pos = pData->curtime;

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

			FileDriver_Play(pobj, pos);
		}
		break;
		case EMedia_Seek_NextPos:
		{
			uint64_t pos = pData->curtime + 1000;

			if (pos < pData->songtime)
				FileDriver_Play(pobj, pos);
		}
		break;
		default: ;
	}
}

static void FileDriver_SetLoopMode(MediaObject* pobj, Media_LoopMode mode)
{
	FileDriver_ObjectData* pData = pobj->m_pData;

	pobj->m_LoopMode = mode;

	_swix(DiskSample_StreamStatus, _IN(0)|_IN(1)|_IN(2), pData->handle
		, ((pobj->m_LoopMode != loop_section_loop) && (pobj->m_LoopMode != loop_volume_loop))
		? 0x8 : 0x0, 0x8);
}

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

static const EMetaId tmetaids[] = {
	  EMetaId_StreamType
	, EMetaId_StreamTitle
	, EMetaId_StreamArtist
	, EMetaId_StreamAlbum
	, EMetaId_StreamStation
	, EMetaId_StreamMimeType
	, EMetaId_StreamDate
	, (EMetaId) 0
};

static const EMetaId imetaids[] = {
	  EMetaId_StreamTrackNumber
	, (EMetaId) 0
};

static void FileDriver_UpdateTexts(MediaObject* pobj, FileDriver_ObjectData* pData)
{
	const _kernel_oserror* e;
	const char* text;
	unsigned int globalstamp = 0;
	unsigned int stamp = 0;
	const EMetaId* pid;

	// extract stream info timestamp
	_swix(DiskSample_StreamParam, _INR(0,1)|_OUT(3), pData->handle, -1, &globalstamp);
	if (pobj->m_timestamp == globalstamp)
		return;

	// extract meta info
	for (pid = &tmetaids[0]; *pid; pid++)
	{
		e = _swix(DiskSample_StreamParam, _INR(0,1)|_OUTR(2,3), pData->handle, *pid, &text, &stamp);

		if (!e && (stamp > pobj->m_timestamp))
		{
			if (*pid == EMetaId_StreamType)
			{
				int bits, ch, rate;

				_swix(DiskSample_StreamParam, _INR(0,1)|_OUT(2), pData->handle, 0, &ch);
				_swix(DiskSample_StreamParam, _INR(0,1)|_OUT(2), pData->handle, 1, &rate);
				_swix(DiskSample_StreamParam, _INR(0,1)|_OUT(2), pData->handle, 2, &bits);

				throw_MediaObject_SetText(pobj, *pid, EMetaOrigin_Stream
					, SPrintf("%s - %d kbits, %d kHz, %d ch", text, bits, rate/1000, ch));
			}
			else
			{
				throw_MediaObject_SetText(pobj, *pid, EMetaOrigin_Stream, text);
			}
		}
	}

	// extract meta info
	for (pid = &imetaids[0]; *pid; pid++)
	{
		int val;
		e = _swix(DiskSample_StreamParam, _INR(0,1)|_OUTR(2,3), pData->handle, *pid, &val, &stamp);

		if (!e && (stamp > pobj->m_timestamp))
		{
			if (*pid == EMetaId_StreamTrackNumber)
			{
				throw_MediaObject_SetMeta(pobj, *pid, EMetaOrigin_Stream, (void*) val, 4);
			}
			else
			{
				throw_MediaObject_SetText(pobj, *pid, EMetaOrigin_Stream, text);
			}
		}
	}

	pobj->m_timestamp = globalstamp;
}

static void FileDriver_FirstStart(MediaObject* pobj, FileDriver_ObjectData* pData)
{
	// extract info
	_swix(DiskSample_StreamDecoding, _INR(0,2), pData->handle, 7, &pData->songtime);

	pobj->m_Status = status_play;
	_swix(DiskSample_StreamPlay, _IN(0), pData->handle);

	FileDriver_UpdateTexts(pobj, pData);

	pobj->m_Updates |= Media_Updated_Length;
}

static Media_Status FileDriver_GetStatus(MediaObject* pobj)
{
	FileDriver_ObjectData* pData = pobj->m_pData;
	const _kernel_oserror* e;
	unsigned int status;

	e = _swix(DiskSample_StreamStatus, _IN(0)|_IN(1)|_IN(2)|_OUT(1), pData->handle, 0, 0, &status);
	if (e) goto error;

	if (!(status & 1))
	{
		// Not ready
		pobj->m_Status = status_busy;
		e = _swix(DiskSample_StreamIsReady, _IN(0), pData->handle);
		if (e) goto error;
	}
	else if (!pData->isReady)
	{
		// Ready for the first time
		pData->isReady = true;
		FileDriver_SetConfig(pobj, Media_CfgFrequency, &pobj->m_iSpeed);
		FileDriver_FirstStart(pobj, pData);
	}
	else if (status & 2)
	{
		// Playing
		pobj->m_Status = status_play;
		FileDriver_UpdateTexts(pobj, pData);
	}
	else if (status & 4)
	{
		// Paused
		// force pause?
		if (pobj->m_Status != status_pause)
			pobj->m_Status = status_stop;
	}
	else
		pobj->m_Status = status_stop;

	return pobj->m_Status;

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

	return pobj->m_Status;
}

static void FileDriver_GetPosition(MediaObject* pobj, MediaPosition* pos)
{
	FileDriver_ObjectData* pData = pobj->m_pData;

	pos->driver.start = 0;
	_swix(DiskSample_StreamDecoding, _INR(0,2), pData->handle, 6, &pos->driver.pos);
	_swix(DiskSample_StreamDecoding, _INR(0,2), pData->handle, 7, &pos->driver.end);

	pData->curtime = pos->driver.pos;
	if (pData->songtime != pos->driver.end)
	{
		pData->songtime = pos->driver.end;
		pobj->m_Updates |= Media_Updated_Length;
	}

	// Convert to cs
	pos->section.pos = (unsigned int) (pos->driver.pos / 10);
	pos->section.len = (unsigned int) (pos->driver.end / 10);
	pos->section.nr = 0;

	pos->volume.sections = 1;
	pos->volume.pos = pos->section.pos;
	pos->volume.len = pos->section.len;
}

static void UrlDriver_GetPosition(MediaObject* pobj, MediaPosition* pos)
{
	FileDriver_ObjectData* pData = pobj->m_pData;

	// use pos->driver as tmp buffer
	_swix(DiskSample_StreamDecoding, _INR(0,2), pData->handle, 2, &pos->driver.pos);
	_swix(DiskSample_StreamDecoding, _INR(0,2), pData->handle, 0, &pos->driver.end);
	pos->driver.end += pos->driver.pos;
	pos->volume.sections = 1;
	pos->volume.pos = (unsigned int) (pos->driver.pos / 10);
	pos->volume.len = (unsigned int) (pos->driver.end / 10);

	pos->driver.start = 0;
	_swix(DiskSample_StreamDecoding, _INR(0,2), pData->handle, 6, &pos->driver.pos);
	_swix(DiskSample_StreamDecoding, _INR(0,2), pData->handle, 7, &pos->driver.end);

	pData->curtime = pos->driver.pos;
	if (pData->songtime != pos->driver.end)
	{
		pData->songtime = pos->driver.end;
		pobj->m_Updates |= Media_Updated_Length;
	}

	// Convert to cs
	pos->section.pos = (unsigned int) (pos->driver.pos / 10);
	pos->section.len = (unsigned int) (pos->driver.end / 10);
	pos->section.nr = 0;
}

static void FileDriver_SetConfig(MediaObject* pobj, Media_Config type, const void* value)
{
	FileDriver_ObjectData* pData = pobj->m_pData;
	unsigned int dummy;

	switch(type)
	{
		case Media_CfgVolume:
		{
			MediaVolume* pvol = (MediaVolume*) value;
			unsigned int val = pvol->sysscaled;

			if (val > 0x40000) val = 0x40000;
			_swix(DiskSample_StreamVolume, _IN(0)|_IN(1), pData->handle, val >> 8);
		}
		break;
		case Media_CfgFrequency:
		{
			pobj->m_iSpeed = *(unsigned int*) value;
			if (pData->isReady)
			{
				int freq = pobj->m_iSpeed;
				_swix(DiskSample_StreamParam, _INR(0,1)|_OUT(2), pData->handle, 1, &freq);
				freq = Options_MixFrequency(Options(), freq);
				// Is 16-bit disabled ?
				_swix(DiskSample_Configure, _IN(0)|_IN(1)|_OUT(1), 4, -1, &dummy);
				if (dummy)
					_swix(DiskSample_Configure, _IN(0)|_IN(1), 0, Quality_FromFrequency(freq));
				else
					_swix(DiskSample_Configure, _IN(0)|_IN(1), 5, freq);
			}
		}
		break;
		case Media_CfgInterpol:
		{
			bool val = *(bool*) value;
			_swix(DiskSample_Configure, _IN(0)|_IN(1), 1, (val != 0));
		}
		break;
		case Media_CfgBalance:
		{
			unsigned int val = *(unsigned int*) value;
			_swix(DiskSample_Configure, _IN(0)|_IN(1), 2, val);
		}
		break;
		case Media_CfgStereoSeparation:
		{
			unsigned int val = *(unsigned int*) value;
			_swix(DiskSample_Configure, _IN(0)|_IN(1), 3, val);
		}
		break;
		case Media_CfgEqualizer:
		{
			COptions* popt = (COptions*) value;
			if (popt->Equalizer.bActive)
				Equalizer_Configure(0x072ec1, &popt->Equalizer.Params);
			else
				Equalizer_Configure(0x072ec1, NULL);
		}
		break;
		case Media_CfgDMASize:
		{
			unsigned int val = *(unsigned int*) value;
			// Sound_Configure
			if (val) _swix(0x40140, _INR(0,4), 0, val, 0, 0, 0);
		}
		break;
		default: ;
	}
}

static unsigned int FileDriver_GetConfig(MediaObject* pobj, Media_Config type)
{
	FileDriver_ObjectData* pData = pobj->m_pData;

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

			if (_swix(DiskSample_StreamVolume, _IN(0)|_IN(1)|_OUT(1), pData->handle, -1, &volume)) return 0xffff;

			return volume << 8;
		}
		break;
		case Media_CfgVolumeScale:
		{
			unsigned int scale;

			if (_swix(DiskSample_Configure, _IN(0)|_OUT(3), 255, &scale))
				return 100;

			return scale;
		}
		break;
		default: ;
	}

	return 0xffffffff;
}

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

const MediaDriver FileDriver =
{
	  FileDriver_Name
	, throw_FileDriver_LoadObject
	, FileDriver_UnLoadObject
	, FileDriver_Chain
	, FileDriver_Play
	, FileDriver_Freeze
	, FileDriver_Stop
	, FileDriver_Seek
	, FileDriver_SetLoopMode
	, FileDriver_GetLoopMode
	, FileDriver_GetStatus
	, FileDriver_GetPosition
	, NULL
	, FileDriver_SetConfig
	, FileDriver_GetConfig
};

const MediaDriver FileExtDriver =
{
	  FileExtDriver_Name
	, throw_FileExtDriver_LoadObject
	, FileDriver_UnLoadObject
	, FileDriver_Chain
	, FileDriver_Play
	, FileDriver_Freeze
	, FileDriver_Stop
	, FileDriver_Seek
	, FileDriver_SetLoopMode
	, FileDriver_GetLoopMode
	, FileDriver_GetStatus
	, FileDriver_GetPosition
	, NULL
	, FileDriver_SetConfig
	, FileDriver_GetConfig
};

const MediaDriver UrlDriver =
{
	  FileDriver_Name
	, throw_UrlDriver_LoadObject
	, FileDriver_UnLoadObject
	, FileDriver_Chain
	, UrlDriver_Play
	, UrlDriver_Freeze
	, FileDriver_Stop
	, UrlDriver_Seek
	, FileDriver_SetLoopMode
	, FileDriver_GetLoopMode
	, FileDriver_GetStatus
	, UrlDriver_GetPosition
	, NULL
	, FileDriver_SetConfig
	, FileDriver_GetConfig
};
