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

extern const MediaDriver PlayItdrv;

typedef struct PlayItdrv_ObjectData
{
	unsigned int	type;
	unsigned int	freq;
	unsigned int	size;
	struct
	{
		const char*	ptr;
		char		data[256];
	} buffer;
} PlayItdrv_ObjectData;

#define PlayIt_Config		0x4d141
#define PlayIt_LoadDriver	0x4d142
#define PlayIt_SampleInfo	0x4d144
#define PlayIt_Status		0x4d145
#define PlayIt_SetPtr		0x4d149
#define PlayIt_Play			0x4d14a
#define PlayIt_Stop			0x4d14b
#define PlayIt_Pause		0x4d14c
#define PlayIt_Balance		0x4d14d
#define PlayIt_SetLoop		0x4d153

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

static const _kernel_oserror* PlayIt_ConfigBuffers(unsigned int size)
{
	return _swix(PlayIt_Config, _IN(0)|_IN(1), 1, size<<10);
}

static unsigned int PlayIt_ConfigFlags(unsigned int mask, unsigned value)
{
	unsigned int val;

	_swix(PlayIt_Config, _INR(0,2)|_OUT(1), 0, mask, value, &val);

	return val;
}

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

	regs.r[0] = set;
	if ((e = _kernel_swi(0x6d146, &regs, &regs)) == NULL)
	{
		if (read)
		{
			if (set)
				*read = set;
			else
				*read = regs.r[0];
		}
	}

	return e;
}

static const _kernel_oserror* PlayIt_Open(const char* filename, unsigned int* type, unsigned int* freq, unsigned int* size)
{
	_kernel_swi_regs	regs;
	const _kernel_oserror*	e;
	int			block[6];

	regs.r[0] = (int) filename;
	regs.r[1] = *type;
	regs.r[2] = *freq;
	regs.r[3] = 0;
	regs.r[4] = -1;
	if ((e = _kernel_swi(0x6d147, &regs, &regs)) != NULL)
		return e;

	// FileInfo, sound format
	regs.r[0] = 0;
	regs.r[1] = 0;
	regs.r[2] = (int) block;
	if ((e = _kernel_swi(0x6d154, &regs, &regs)) == NULL)
	{
		*type = block[0];
		*freq = block[1];
		*size = block[5];
	}

	return e;
}

static const _kernel_oserror* PlayIt_FileInfo(unsigned int code, PlayItdrv_ObjectData* pData)
{
	_kernel_swi_regs	regs;

	regs.r[0] = 1;
	regs.r[1] = 0;
	regs.r[2] = (int) &pData->buffer;
	regs.r[3] = sizeof(pData->buffer);
	regs.r[4] = 1 << code;
	return _kernel_swi(0x6d154, &regs, &regs);
}

static const _kernel_oserror* PlayIt_ClientOp(unsigned int code, unsigned int* count)
{
	_kernel_swi_regs	regs;
	const _kernel_oserror*	e;

	regs.r[0] = code;
	regs.r[1] = 1;
	regs.r[2] = 0;
	if ((e = _kernel_swi(0x6d155, &regs, &regs)) == NULL)
	{
		if (count) *count = regs.r[1];
	}

	return e;
}

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

static unsigned int PlayIt_GetCurrentPos(void)
{
	unsigned int pos;
	unsigned int status;

	if (_swix(PlayIt_Status, _OUT(0)|_OUT(1), &status, &pos))
		return 0;

	if (status & 1)
		return pos;

	return 0;
}

static const char PlayItdrv_Name[] = "PlayIt";

static const char *names[] =
{
	"Unknown",
	"Armadeus",
	"Wave",
	"Sun Audio",
	"AudioWorks",
	"Psion A-law",
	"Audio IFF",
	"Amiga IFF",
	"Infocom",
	"VOC",
	"ARMovie",
	"DataVox",
	"CDAudio",
	"Symphony"
};
static const char *types[] =
{
	"8-bit s-lin",
	"8-bit u-lin",
	"8-bit A-law",
	"8-bit -law",
	"8-bit VIDC",
	"Unknown",
	"Unknown",
	"Unknown",
	"16-bit s-lin",
	"16-bit u-lin",
	"16-bit s-lin big-end",
	"16-bit u-lin big-end",
	"12-bit s-lin",
	"12-bit u-lin",
	"12-bit s-lin big-end",
	"12-bit u-lin big-end",
	"4-bit MS ADPCM",
	"4-bit IMA ADPCM",
	"Unknown",
	"4-bit IMA ADPCM big-end",
	"Unknown",
	"3-bit IMA ADPCM",
	"Unknown",
	"Unknown",
	"Unknown",
	"2-bit IMA ADPCM",
	"Unknown",
	"2-bit IMA ADPCM big-end"
};

static MediaObject* inUse = NULL;

static bool throw_PlayItdrv_LoadObject(MediaObject* pobj, const char* pObjectName)
{
	PlayItdrv_ObjectData* volatile	pData = NULL;
	const _kernel_oserror*		err;
	int			format, ftype, channels;

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

	pData = throw_mem_alloc(sizeof(PlayItdrv_ObjectData));

	Media_Attach(pobj, pData, Media_HType_File);

	// Ensure modules are loaded
	throw_os(RMEnsure(PlayItdrv_Name, 148, "PlayIt:PlayIt"));

	throw_os(_swix(PlayIt_LoadDriver, _IN(0), "PlayIt:"));

	PlayIt_ConfigBuffers(Options()->Drivers.PlayIt.BufferSize);

	// stop playing and load a new file
	pData->type = 0; // auto-detect
	err= PlayIt_Open(pObjectName, &pData->type, &pData->freq, &pData->size);
	if (err)
	{
		if (Options()->Drivers.PlayIt.bInterpretUnknown)
		{
			pData->type = Options()->Drivers.PlayIt.Format + (Options()->Drivers.PlayIt.Channels << 8);
			pData->freq = Options()->Drivers.PlayIt.Frequency;
			pData->size = 0;
			throw_os(PlayIt_Open(pObjectName, &pData->type, &pData->freq, &pData->size));
		}
		else throw_os(err);
	}
	inUse = pobj;

	PlayIt_ClientOp(0, NULL);

	// extract title info
	throw_os(PlayIt_FileInfo(0, pData));

	// strip leading and trailing blanks
	if (strlen(pData->buffer.ptr) > 0)
		throw_MediaObject_SetText(pobj, EMetaId_StreamTitle, EMetaOrigin_Stream, pData->buffer.ptr);

	throw_os(PlayIt_FileInfo(1, pData));

	// strip leading and trailing blanks
	if (strlen(pData->buffer.ptr) > 0)
		throw_MediaObject_SetText(pobj, EMetaId_StreamArtist, EMetaOrigin_Stream, pData->buffer.ptr);

	ftype = pData->type >> 24;
	if (ftype > 0xd)
	{
		throw_MediaObject_SetText(pobj, EMetaId_StreamType, EMetaOrigin_Stream, names[0]);
	}
	else
	{
		format = pData->type & 0xff;
		if (format > 0x11)
		{
			throw_MediaObject_SetText(pobj, EMetaId_StreamType, EMetaOrigin_Stream, types[5]);
		}
		else
		{
			channels = (pData->type & 0xf00) >> 8;

			throw_MediaObject_SetText(pobj, EMetaId_StreamType, EMetaOrigin_Stream
				, SPrintf("%s %s %d kHz - %d Ch", names[ftype], types[format], pData->freq/1000, channels));
		}
	}
	throw_MediaObject_SetText(pobj, EMetaId_ModuleName, EMetaOrigin_Stream, PlayItdrv_Name);
	throw_MediaObject_SetMeta(pobj, EMetaId_ModuleHandle, EMetaOrigin_Stream, (void*) 0, 0);

	return true;
}

static void PlayItdrv_UnLoadObject(MediaObject* pobj)
{
	PlayItdrv_ObjectData* pData = pobj->m_pData;
	unsigned int count;

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

	if (inUse == pobj)
	{
		inUse = NULL;
		_swix(PlayIt_Stop, 0);
		PlayIt_ClientOp(1, &count);

		if ((!count)
		&&  Options()->Music_Files.bUnloadModules)
			RMKill("PlayIt");
	}
}

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

	// if pos = 0, song is either started or restarted
	if (pos == 0)
	{
		_swix(PlayIt_Pause, 0);
		PlayIt_ConfigFlags(0x1, 0x1);
	}
	if (pobj->m_Status == status_play)
		_swix(PlayIt_SetPtr, _IN(0), pos);
	pobj->m_Status = status_play;
	_swix(PlayIt_Play, 0);
}

static void PlayItdrv_Freeze(MediaObject* pobj, bool on)
{
	if (on)
	{
		_swix(PlayIt_Pause, 0);
		pobj->m_Status = status_pause;
	}
	else
	{
		_swix(PlayIt_Play, 0);
		pobj->m_Status = status_play;
	}
}

static void PlayItdrv_Stop(MediaObject* pobj)
{
	_swix(PlayIt_Pause, 0);
	pobj->m_Status = status_stop;
}

static void PlayItdrv_Seek(MediaObject* pobj, EMedia_Seek set)
{
	switch(set)
	{
		case EMedia_Seek_Start:
		case EMedia_Seek_SectionStart:
		{
			PlayItdrv_Play(pobj, 0);
		}
		break;
		case EMedia_Seek_PreviousPos:
		{
			PlayItdrv_ObjectData* pData = pobj->m_pData;
			unsigned int pos = PlayIt_GetCurrentPos();

			if (pos < pData->freq)
				pos = 0;
			else
				pos -= pData->freq;

			PlayItdrv_Play(pobj, (uint64_t) pos);
		}
		break;
		case EMedia_Seek_NextPos:
		{
			PlayItdrv_ObjectData* pData = pobj->m_pData;
			unsigned int pos = PlayIt_GetCurrentPos() + pData->freq;

			if (pos < pData->size)
				PlayItdrv_Play(pobj, (uint64_t) pos);
		}
		break;
		default: ;
	}
}

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

	pobj->m_LoopMode = mode;

	if ((pobj->m_LoopMode == loop_section_loop)
	||  (pobj->m_LoopMode == loop_volume_loop))
		_swix(PlayIt_SetLoop, _IN(0)|_IN(1)|_IN(2), 0, pData->size, 0);
	else
		_swix(PlayIt_SetLoop, _IN(0)|_IN(1)|_IN(2), 0, 0, 0);
}

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

static Media_Status PlayItdrv_GetStatus(MediaObject* pobj)
{
	unsigned int status;
	const _kernel_oserror* e;

	// Stop warnings
	IGNORE(pobj);

/* Unfortunately reports a 'No file open' error while paying end of file
	// detects file being unloaded
	e = _swix(PlayIt_SampleInfo, 0);
	if (e) goto error;
*/

	e = _swix(PlayIt_Status, _OUT(0), &status);
	if (e) goto error;

	if (status & 1)
		return status_play;
	if (status & 2)
		return status_pause;

	return status_stop;

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

	return pobj->m_Status;
}

static void PlayItdrv_GetPosition(MediaObject* pobj, MediaPosition* pos)
{
	PlayItdrv_ObjectData* pData = pobj->m_pData;
	unsigned int pos1;
	unsigned int status;

	if (_swix(PlayIt_Status, _OUT(0)|_OUT(1), &status, &pos1)
	||  !(status & 1))
		pos1 = 0;

	pos->driver.start = 0;
	pos->driver.pos = (uint64_t) pos1;
	pos->driver.end = (uint64_t) pData->size;
	pos->volume.sections = 1;
	if (pData->freq)
	{
		uint64_t val;

		val = pos1;
		val *= 100;
		val /= pData->freq;
		pos->volume.pos = (unsigned int) val;

		val = pData->size;
		val *= 100;
		val /= pData->freq;
		pos->volume.len = (unsigned int) val;
	}
	else
	{
		pos->volume.pos = 0;
		pos->volume.len = 0;
	}
	pos->section.pos = pos->volume.pos;
	pos->section.len = pos->volume.len;
	pos->section.nr = 0;
}

static void PlayItdrv_SetConfig(MediaObject* pobj, Media_Config type, const void* value)
{
	// Stop warnings
	IGNORE(pobj);

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

			// Flag to make independant of system volume
			// because in system dependant mode, PlayIt makes a mess of volume
			PlayIt_ConfigFlags(1 << 2, 0);

			// convert to log
			if (val > 0)
				val = ((int) (16.*log(val)/log(2))) - 129;
			else
				val = 0;
			if (val > 255) val = 255; // values above 127 are amplified
			if (val <= 0) val = 1;
			PlayIt_Volume(val, NULL);
		}
		break;
		case Media_CfgInterpol:
		{
			bool val = 1 && (*(bool*) value);
			PlayIt_ConfigFlags(1 << 4, val << 4);
		}
		break;
		case Media_CfgBalance:
		{
			unsigned int val = *(unsigned int*) value;
			if (val == 0) val = 1;
			_swix(PlayIt_Balance, _IN(0), val - 128);
		}
		break;
		default: ;
	}
}

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

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

			if (PlayIt_Volume(0, &volume)) return 0xffff;
			if (volume <= 1) return 0;
			volume = (int)(256 * pow(1.044273782, volume));
			return volume;
		}
		break;
		default: ;
	}

	return 0xffffffff;
}

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

const MediaDriver PlayItdrv =
{
	  PlayItdrv_Name
	, throw_PlayItdrv_LoadObject
	, PlayItdrv_UnLoadObject
	, NULL
	, PlayItdrv_Play
	, PlayItdrv_Freeze
	, PlayItdrv_Stop
	, PlayItdrv_Seek
	, PlayItdrv_SetLoopMode
	, PlayItdrv_GetLoopMode
	, PlayItdrv_GetStatus
	, PlayItdrv_GetPosition
	, NULL
	, PlayItdrv_SetConfig
	, PlayItdrv_GetConfig
};
