/*
** music_spc.cpp
** SPC codec for FMOD Ex, using snes_spc for decoding.
**
**---------------------------------------------------------------------------
** Copyright 2008 Randy Heit
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
**    notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
**    notice, this list of conditions and the following disclaimer in the
**    documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
**    derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/

// HEADER FILES ------------------------------------------------------------

#include "templates.h"
#include "c_cvars.h"
#include "SNES_SPC.h"
#include "SPC_Filter.h"
#ifndef NO_SOUND
#include "fmod_wrap.h"
#endif
#include "doomdef.h"
#include "m_swap.h"
#include "m_fixed.h"

// MACROS ------------------------------------------------------------------

// TYPES -------------------------------------------------------------------

#ifndef NO_SOUND

struct XID6Tag
{
	BYTE ID;
	BYTE Type;
	WORD Value;
};

struct SPCCodecData
{
	SNES_SPC *SPC;
	SPC_Filter *Filter;
	FMOD_CODEC_WAVEFORMAT WaveFormat;
	bool bPlayed;
};

// EXTERNAL FUNCTION PROTOTYPES --------------------------------------------

// PUBLIC FUNCTION PROTOTYPES ----------------------------------------------

FMOD_RESULT SPC_CreateCodec(FMOD::System *sys);

// PRIVATE FUNCTION PROTOTYPES ---------------------------------------------

static FMOD_RESULT SPC_DoOpen(FMOD_CODEC_STATE *codec, SPCCodecData *userdata);
static FMOD_RESULT F_CALLBACK SPC_Open(FMOD_CODEC_STATE *codec, FMOD_MODE usermode, FMOD_CREATESOUNDEXINFO *userexinfo);
static FMOD_RESULT F_CALLBACK SPC_Close(FMOD_CODEC_STATE *codec);
static FMOD_RESULT F_CALLBACK SPC_Read(FMOD_CODEC_STATE *codec, void *buffer, unsigned int size, unsigned int *read);
static FMOD_RESULT F_CALLBACK SPC_SetPosition(FMOD_CODEC_STATE *codec_state, int subsound, unsigned int position, FMOD_TIMEUNIT postype);
#endif // NO_SOUND

// EXTERNAL DATA DECLARATIONS ----------------------------------------------

EXTERN_CVAR (Int, snd_samplerate)

// PUBLIC DATA DEFINITIONS -------------------------------------------------

CVAR (Float, spc_amp, 1.875f, CVAR_ARCHIVE|CVAR_GLOBALCONFIG)

// PRIVATE DATA DEFINITIONS ------------------------------------------------

#ifndef NO_SOUND

static FMOD_CODEC_DESCRIPTION SPCCodecDescription = {
	"Super Nintendo SPC File",
	0x00000001,
	1,
	FMOD_TIMEUNIT_MS | FMOD_TIMEUNIT_PCM,
	SPC_Open,
	SPC_Close,
	SPC_Read,
	NULL,
	SPC_SetPosition,
	NULL,
	NULL,
	NULL
};

// CODE --------------------------------------------------------------------

//==========================================================================
//
// SPC_CreateCodec
//
//==========================================================================

FMOD_RESULT SPC_CreateCodec(FMOD::System *sys)
{
	return sys->createCodec(&SPCCodecDescription);
}

//==========================================================================
//
// SPC_Open
//
//==========================================================================

static FMOD_RESULT SPC_DoOpen(FMOD_CODEC_STATE *codec, SPCCodecData *userdata)
{
	FMOD_RESULT result;
	char spcfile[SNES_SPC::spc_file_size];
	unsigned int bytesread;
	SNES_SPC *spc;
	SPC_Filter *filter;

	if (codec->filesize < 66048)
	{ // Anything smaller than this is definitely not a (valid) SPC.
		return FMOD_ERR_FORMAT;
	}
	result = codec->fileseek(codec->filehandle, 0, 0);
	if (result != FMOD_OK)
	{
		return result;
	}
	// Check the signature.
	result = codec->fileread(codec->filehandle, spcfile, SNES_SPC::signature_size, &bytesread, NULL);
	if (result != FMOD_OK || bytesread != SNES_SPC::signature_size ||
		memcmp(spcfile, SNES_SPC::get_signature(), SNES_SPC::signature_size) != 0)
	{
		result = FMOD_ERR_FORMAT;
		// If the length is right and this is just a different version, try
		// to pretend it's the current one.
		if (bytesread == SNES_SPC::signature_size)
		{
			memcpy(spcfile + 28, SNES_SPC::get_signature() + 28, 7);
			if (memcmp(spcfile, SNES_SPC::get_signature(), SNES_SPC::signature_size) == 0)
			{
				result = FMOD_OK;
			}
		}
	}
	if (result != FMOD_OK)
	{
		return result;
	}
	// Load the rest of the file.
	result = codec->fileread(codec->filehandle, spcfile + SNES_SPC::signature_size,
		SNES_SPC::spc_file_size - SNES_SPC::signature_size, &bytesread, NULL);
	if (result != FMOD_OK || bytesread < SNES_SPC::spc_min_file_size - SNES_SPC::signature_size)
	{
		return FMOD_ERR_FILE_BAD;
	}
	bytesread += SNES_SPC::signature_size;

	// Create the SPC.
	spc = (userdata != NULL) ? userdata->SPC : new SNES_SPC;
	spc->init();
	if (spc->load_spc(spcfile, bytesread) != NULL)
	{
		if (userdata != NULL)
		{
			delete spc;
		}
		return FMOD_ERR_FILE_BAD;
	}
	spc->clear_echo();

	// Create the filter.
	filter = (userdata != NULL) ? userdata->Filter : new SPC_Filter;
	filter->set_gain(int(SPC_Filter::gain_unit * spc_amp));

	// Search for amplification tag in extended ID666 info.
	if (codec->filesize > SNES_SPC::spc_file_size + 8 && bytesread == SNES_SPC::spc_file_size)
	{
		DWORD id;

		result = codec->fileread(codec->filehandle, &id, 4, &bytesread, NULL);
		if (result == FMOD_OK && bytesread == 4 && id == MAKE_ID('x','i','d','6'))
		{
			DWORD size;

			result = codec->fileread(codec->filehandle, &size, 4, &bytesread, NULL);
			if (result == FMOD_OK && bytesread == 4)
			{
				size = LittleLong(size);
				DWORD pos = SNES_SPC::spc_file_size + 8;

				while (pos < size)
				{
					XID6Tag tag;

					result = codec->fileread(codec->filehandle, &tag, 4, &bytesread, NULL);
					if (result != FMOD_OK || bytesread != 4)
					{
						break;
					}
					if (tag.Type == 0)
					{
						// Don't care about these
					}
					else
					{
						if ((pos += LittleShort(tag.Value)) <= size)
						{
							if (tag.Type == 4 && tag.ID == 0x36)
							{
								DWORD amp;
								result = codec->fileread(codec->filehandle, &amp, 4, &bytesread, NULL);
								if (result == FMOD_OK && bytesread == 4)
								{
									filter->set_gain(LittleLong(amp) >> 8);
								}
								break;
							}
						}
						if (FMOD_OK != codec->fileseek(codec->filehandle, pos, NULL))
						{
							break;
						}
					}
				}
			}
		}
	}
	if (userdata == NULL)
	{
		userdata = new SPCCodecData;
		userdata->SPC = spc;
		userdata->Filter = filter;
		memset(&userdata->WaveFormat, 0, sizeof(userdata->WaveFormat));
		userdata->WaveFormat.format = FMOD_SOUND_FORMAT_PCM16;
		userdata->WaveFormat.channels = 2;
		userdata->WaveFormat.frequency = SNES_SPC::sample_rate;
		userdata->WaveFormat.lengthbytes = SNES_SPC::spc_file_size;
		userdata->WaveFormat.lengthpcm = ~0u;
		userdata->WaveFormat.blockalign = 4;
		codec->numsubsounds = 0;
		codec->waveformat = &userdata->WaveFormat;
		codec->plugindata = userdata;
	}
	userdata->bPlayed = false;
	return FMOD_OK;
}

//==========================================================================
//
// SPC_Open
//
//==========================================================================

static FMOD_RESULT F_CALLBACK SPC_Open(FMOD_CODEC_STATE *codec, FMOD_MODE usermode, FMOD_CREATESOUNDEXINFO *userexinfo)
{
	return SPC_DoOpen(codec, NULL);
}

//==========================================================================
//
// SPC_Close
//
//==========================================================================

static FMOD_RESULT F_CALLBACK SPC_Close(FMOD_CODEC_STATE *codec)
{
	SPCCodecData *data = (SPCCodecData *)codec->plugindata;
	if (data != NULL)
	{
		delete data->Filter;
		delete data->SPC;
		delete data;
	}
	return FMOD_OK;
}

//==========================================================================
//
// SPC_Read
//
//==========================================================================

static FMOD_RESULT F_CALLBACK SPC_Read(FMOD_CODEC_STATE *codec, void *buffer, unsigned int size, unsigned int *read)
{
	SPCCodecData *data = (SPCCodecData *)codec->plugindata;
	if (read != NULL)
	{
		*read = size;
	}
	data->bPlayed = true;
	if (data->SPC->play(size >> 1, (short *)buffer) != NULL)
	{
		return FMOD_ERR_PLUGIN;
	}
	data->Filter->run((short *)buffer, size >> 1);
	return FMOD_OK;
}

static FMOD_RESULT F_CALLBACK SPC_SetPosition(FMOD_CODEC_STATE *codec, int subsound, unsigned int position, FMOD_TIMEUNIT postype)
{
	SPCCodecData *data = (SPCCodecData *)codec->plugindata;
	FMOD_RESULT result;

	if (data->bPlayed)
	{ // Must reload the file after it's already played something.
		result = SPC_DoOpen(codec, data);
		if (result != FMOD_OK)
		{
			return result;
		}
	}
	if (position != 0)
	{
		if (postype == FMOD_TIMEUNIT_PCM)
		{
			data->SPC->skip(position * 4);
		}
		else if (postype == FMOD_TIMEUNIT_MS)
		{
			data->SPC->skip(Scale(position, SNES_SPC::sample_rate, 1000) * 4);
		}
		else
		{
			return FMOD_ERR_UNSUPPORTED;
		}
	}
	return FMOD_OK;
}

#endif // NO_SOUND
