/*
snd_mp3.c - mp3 format loading and streaming
Copyright (C) 2010 Uncle Mike

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
*/

#include "soundlib.h"
#include "libmpg/libmpg.h"

#pragma pack( push, 1 )
typedef struct did3v2_header_s
{
	char     ident[3];  // must be "ID3"
	uint8_t  major_ver; // must be 4
	uint8_t  minor_ver; // must be 0
	uint8_t  flags;
	uint32_t length; // size of extended header, padding and frames
} did3v2_header_t;
STATIC_ASSERT( sizeof( did3v2_header_t ) == 10,
	"invalid did3v2_header_t size" );

typedef struct did3v2_extended_header_s
{
	uint32_t length;
	uint8_t  flags_length;
	uint8_t  flags[1];
} did3v2_extended_header_t;
STATIC_ASSERT( sizeof( did3v2_extended_header_t ) == 6,
	"invalid did3v2_extended_header_t size" );

typedef struct did3v2_frame_s
{
	char     frame_id[4];
	uint32_t length;
	uint8_t  flags[2];
} did3v2_frame_t;
STATIC_ASSERT( sizeof( did3v2_frame_t ) == 10,
	"invalid did3v2_frame_t size" );
#pragma pack( pop )

typedef enum did3v2_header_flags_e
{
	ID3V2_HEADER_UNSYHCHRONIZATION = BIT( 7U ),
	ID3V2_HEADER_EXTENDED_HEADER   = BIT( 6U ),
	ID3V2_HEADER_EXPERIMENTAL      = BIT( 5U ),
	ID3V2_HEADER_FOOTER_PRESENT    = BIT( 4U ),
} did3v2_header_flags_t;

#define CHECK_IDENT( ident, b0, b1, b2 )        ((( ident )[0]) == ( b0 ) && (( ident )[1]) == ( b1 ) && (( ident )[2]) == ( b2 ))
#define CHECK_FRAME_ID( ident, b0, b1, b2, b3 ) ( CHECK_IDENT( ident, b0, b1, b2 ) && (( ident )[3]) == ( b3 ))

static uint32_t Sound_ParseSynchInteger( uint32_t v )
{
	uint32_t res = 0;

	// read as big endian
	res |= (( v >> 24 ) & 0x7f ) << 0;
	res |= (( v >> 16 ) & 0x7f ) << 7;
	res |= (( v >> 8  ) & 0x7f ) << 14;
	res |= (( v >> 0  ) & 0x7f ) << 21;

	return res;
}

static void Sound_HandleCustomID3Comment( const char *key, const char *value )
{
	if( !Q_strcmp( key, "LOOP_START" ) || !Q_strcmp( key, "LOOPSTART" ))
		sound.loopstart = Q_atoi( value );
	// unknown comment is not an error
}

static qboolean Sound_ParseID3Frame( const did3v2_frame_t *frame, const byte *buffer, size_t frame_length )
{
	if( CHECK_FRAME_ID( frame->frame_id, 'T', 'X', 'X', 'X' ))
	{
		string key, value;
		int32_t key_len, value_len;

		if( buffer[0] == 0x00 || buffer[0] == 0x03 )
		{
			key_len = Q_strncpy( key, &buffer[1], sizeof( key ));
			value_len = frame_length - (1 + key_len + 1);
			if( value_len <= 0 || value_len >= sizeof( value ))
			{
				Con_Printf( S_ERROR "Sound_ParseID3Frame: invalid TXXX description, possibly broken file.\n" );
				return false;
			}

			memcpy( value, &buffer[1 + key_len + 1], value_len );
			value[value_len + 1] = 0;

			Sound_HandleCustomID3Comment( key, value );
		}
		else
		{
			if( buffer[0] == 0x01 || buffer[0] == 0x02 ) // UTF-16 with BOM
				Con_Printf( S_ERROR "Sound_ParseID3Frame: UTF-16 encoding is unsupported. Use UTF-8 or ISO-8859!\n" );
			else
				Con_Printf( S_ERROR "Sound_ParseID3Frame: unknown TXXX tag encoding %d, possibly broken file.\n", buffer[0] );
			return false;
		}
	}

	return true;
}

static qboolean Sound_ParseID3Tag( const byte *buffer, fs_offset_t filesize )
{
	const did3v2_header_t *header = (const did3v2_header_t *)buffer;
	const byte *buffer_begin = buffer;
	uint32_t tag_length;

	if( filesize < sizeof( *header ))
		 return false;

	buffer += sizeof( *header );

	// support only id3v2
	if( !CHECK_IDENT( header->ident, 'I', 'D', '3' ))
	{
		// old id3v1 header found
		if( CHECK_IDENT( header->ident, 'T', 'A', 'G' ))
			Con_Printf( S_ERROR "Sound_ParseID3Tag: ID3v1 is not supported! Convert to ID3v2.4!\n" );

		return true; // missing tag header is not an error
	}

	// support only latest id3 v2.4
	if( header->major_ver != 4 || header->minor_ver == 0xff )
	{
		Con_Printf( S_ERROR "Sound_ParseID3Tag: invalid ID3v2 tag version 2.%d.%d. Convert to ID3v2.4!\n", header->major_ver, header->minor_ver );
		return false;
	}

	tag_length = Sound_ParseSynchInteger( header->length );
	if( tag_length > filesize - sizeof( *header ))
	{
		Con_Printf( S_ERROR "Sound_ParseID3Tag: invalid tag length %u, possibly broken file.\n", tag_length );
		return false;
	}

	// just skip extended header
	if( FBitSet( header->flags, ID3V2_HEADER_EXTENDED_HEADER ))
	{
		const did3v2_extended_header_t *ext_header = (const did3v2_extended_header_t *)buffer;
		uint32_t ext_length = Sound_ParseSynchInteger( ext_header->length );

		if( ext_length > tag_length )
		{
			Con_Printf( S_ERROR "Sound_ParseID3Tag: invalid extended header length %u, possibly broken file.\n", ext_length );
			return false;
		}

		buffer += ext_length;
	}

	while( buffer - buffer_begin < tag_length )
	{
		const did3v2_frame_t *frame = (const did3v2_frame_t *)buffer;
		uint32_t frame_length = Sound_ParseSynchInteger( frame->length );

		if( frame_length > tag_length )
		{
			Con_Printf( S_ERROR "Sound_ParseID3Tag: invalid frame length %u, possibly broken file.\n", frame_length );
			return false;
		}

		buffer += sizeof( *frame );

		// parse can fail, but it's ok to continue
		Sound_ParseID3Frame( frame, buffer, frame_length );

		buffer += frame_length;
	}

	return true;
}

#if XASH_ENGINE_TESTS
int EXPORT Fuzz_Sound_ParseID3Tag( const uint8_t *Data, size_t Size )
{
	memset( &sound, 0, sizeof( sound ));
	Sound_ParseID3Tag( Data, Size );
	return 0;
}
#endif

/*
=================================================================

	MPEG decompression

=================================================================
*/
qboolean Sound_LoadMPG( const char *name, const byte *buffer, fs_offset_t filesize )
{
	void	*mpeg;
	size_t	pos = 0;
	size_t	bytesWrite = 0;
	byte	out[OUTBUF_SIZE];
	size_t	outsize, padsize;
	int	ret;
	wavinfo_t	sc;

	// load the file
	if( !buffer || filesize < FRAME_SIZE )
		return false;

	// couldn't create decoder
	if(( mpeg = create_decoder( &ret )) == NULL )
		return false;

	if( ret ) Con_DPrintf( S_ERROR "%s\n", get_error( mpeg ));

	// trying to read header
	if( !feed_mpeg_header( mpeg, buffer, FRAME_SIZE, filesize, &sc ))
	{
		Con_DPrintf( S_ERROR "Sound_LoadMPG: failed to load (%s): %s\n", name, get_error( mpeg ));
		close_decoder( mpeg );
		return false;
	}

	sound.channels = sc.channels;
	sound.rate = sc.rate;
	sound.width = 2; // always 16-bit PCM
	sound.loopstart = -1;
	sound.size = ( sound.channels * sound.rate * sound.width ) * ( sc.playtime / 1000 ); // in bytes
	padsize = sound.size % FRAME_SIZE;
	pos += FRAME_SIZE; // evaluate pos

	if( !Sound_ParseID3Tag( buffer, filesize ))
	{
		Con_DPrintf( S_WARN "Sound_LoadMPG: (%s) failed to extract LOOP_START tag\n", name );
		sound.loopstart = -1;
	}

	if( !sound.size )
	{
		// bad mpeg file ?
		Con_DPrintf( S_ERROR "Sound_LoadMPG: (%s) is probably corrupted\n", name );
		close_decoder( mpeg );
		return false;
	}

	// add sentinel make sure we not overrun
	sound.wav = (byte *)Mem_Calloc( host.soundpool, sound.size + padsize );
	sound.type = WF_PCMDATA;

	// decompress mpg into pcm wav format
	while( bytesWrite < sound.size )
	{
		int	size;

		if( feed_mpeg_stream( mpeg, NULL, 0, out, &outsize ) != MP3_OK && outsize <= 0 )
		{
			const byte *data = buffer + pos;
			int	bufsize;

			// if there are no bytes remainig so we can decompress the new frame
			if( pos + FRAME_SIZE > filesize )
				bufsize = ( filesize - pos );
			else bufsize = FRAME_SIZE;
			pos += bufsize;

			if( feed_mpeg_stream( mpeg, data, bufsize, out, &outsize ) != MP3_OK )
				break; // there was end of the stream
		}

		if( bytesWrite + outsize > sound.size )
			size = ( sound.size - bytesWrite );
		else size = outsize;

		memcpy( &sound.wav[bytesWrite], out, size );
		bytesWrite += size;
	}

	sound.samples = bytesWrite / ( sound.width * sound.channels );
	close_decoder( mpeg );

	return true;
}

/*
=================
FS_SeekEx
=================
*/
fs_offset_t FS_SeekEx( file_t *file, fs_offset_t offset, int whence )
{
	return FS_Seek( file, offset, whence ) == -1 ? -1 : FS_Tell( file );
}

/*
=================
Stream_OpenMPG
=================
*/
stream_t *Stream_OpenMPG( const char *filename )
{
	stream_t	*stream;
	void	*mpeg;
	file_t	*file;
	int	ret;
	wavinfo_t	sc;

	file = FS_Open( filename, "rb", false );
	if( !file ) return NULL;

	// at this point we have valid stream
	stream = Mem_Calloc( host.soundpool, sizeof( stream_t ));
	stream->file = file;
	stream->pos = 0;

	// couldn't create decoder
	if(( mpeg = create_decoder( &ret )) == NULL )
	{
		Con_DPrintf( S_ERROR "Stream_OpenMPG: couldn't create decoder: %s\n", get_error( mpeg ) );
		Mem_Free( stream );
		FS_Close( file );
		return NULL;
	}

	if( ret ) Con_DPrintf( S_ERROR "%s\n", get_error( mpeg ));

	// trying to open stream and read header
	if( !open_mpeg_stream( mpeg, file, (void*)FS_Read, (void*)FS_SeekEx, &sc ))
	{
		Con_DPrintf( S_ERROR "Stream_OpenMPG: failed to load (%s): %s\n", filename, get_error( mpeg ));
		close_decoder( mpeg );
		Mem_Free( stream );
		FS_Close( file );

		return NULL;
	}

	stream->buffsize = 0; // how many samples left from previous frame
	stream->channels = sc.channels;
	stream->rate = sc.rate;
	stream->width = 2;	// always 16 bit
	stream->ptr = mpeg;
	stream->type = WF_MPGDATA;

	return stream;
}

/*
=================
Stream_ReadMPG

assume stream is valid
=================
*/
int Stream_ReadMPG( stream_t *stream, int needBytes, void *buffer )
{
	// buffer handling
	int	bytesWritten = 0;
	void	*mpg;

	mpg = stream->ptr;

	while( 1 )
	{
		byte	*data;
		int	outsize;

		if( !stream->buffsize )
		{
			if( read_mpeg_stream( mpg, (byte*)stream->temp, &stream->pos ) != MP3_OK )
				break; // there was end of the stream
		}

		// check remaining size
		if( bytesWritten + stream->pos > needBytes )
			outsize = ( needBytes - bytesWritten );
		else outsize = stream->pos;

		// copy raw sample to output buffer
		data = (byte *)buffer + bytesWritten;
		memcpy( data, &stream->temp[stream->buffsize], outsize );
		bytesWritten += outsize;
		stream->pos -= outsize;
		stream->buffsize += outsize;

		// continue from this sample on a next call
		if( bytesWritten >= needBytes )
			return bytesWritten;

		stream->buffsize = 0; // no bytes remaining
	}

	return 0;
}

/*
=================
Stream_SetPosMPG

assume stream is valid
=================
*/
int Stream_SetPosMPG( stream_t *stream, int newpos )
{
	if( set_stream_pos( stream->ptr, newpos ) != -1 )
	{
		// flush any previous data
		stream->buffsize = 0;
		return true;
	}

	// failed to seek for some reasons
	return false;
}

/*
=================
Stream_GetPosMPG

assume stream is valid
=================
*/
int Stream_GetPosMPG( stream_t *stream )
{
	return get_stream_pos( stream->ptr );
}

/*
=================
Stream_FreeMPG

assume stream is valid
=================
*/
void Stream_FreeMPG( stream_t *stream )
{
	if( stream->ptr )
	{
		close_decoder( stream->ptr );
		stream->ptr = NULL;
	}

	if( stream->file )
	{
		FS_Close( stream->file );
		stream->file = NULL;
	}

	Mem_Free( stream );
}
