#include "fmod_settings.h"

#ifdef FMOD_SUPPORT_TREMOR

#include "fmod.h"
#include "fmod_codec_tremor.h"
#include "fmod_codec_wav.h"
#include "fmod_debug.h"
#include "fmod_file.h"
#include "fmod_metadata.h"
#include "fmod_string.h"

//#include "../lib/ogg_vorbis/vorbis/lib/window.h"

#include <string.h>


/*
    ***************************************************************************************************************
    ***************************************************************************************************************
    ***************************************************************************************************************

    NOTES ON OPTIMIZATION
    ---------------------

    - Throw away floor0 and res0 and res1.  Only keep floor1 and res2.  
    - Special case all the transform stuff to the two blocksizes used (if you use two of-em at all).
    - You can also make smaller or algoritmically decodable Huffman tables instead of the big lookups, 
    if you so choose (you'll have to tune the encoder to generate correct streams for you, in that case, though).  
    - You can even hardwire the modes into the decoder (no headers needed anymore --> good for small samples, no setup
    overhead).

    - Remove floor 0: Floor 0 is not to be considered deprecated, but it is of
      limited modern use. No known Vorbis encoder past Xiph.Org's own beta
      4 makes use of floor 0. Floor 1 is also considerably less expensive to
      decode than floor 0 [9]. Removing support for floor 0 will preserve
      memory.
    - Window sizes: Remove all look-up tables for IMDCT support of
      window lengths other than 256 and 2048. This is possible since these are
      the only windows lengths that is used by most (if not all) encoders. Since
      the current Vorbis standard features window lengths of up to 8192
      samples the memory preserve is significant.
    - Low accuracy: If Tremor is compiled with _LOW_ACCURACY_
      defined it will run in low accuracy mode, which means that all
      calculations during decode is limited to a maximum of 32 bit precision.
      Otherwise, 64 bit values are used to store intermediate 32-bit multiply
      results. And the look up tables for windows and IMDCT are converted
      from 32 bits to 8 bits. Sound quality will be reduced but the processor
      load will be reduced.
    - Rewrite data structures: To further reduce memory usage, and possibly
      gain some speed, data structures can be optimized by determining the
      actual needed sizes as well as removing unnecessary or redundant
      members. And due to the alignment restrictions of some processor
      architectures, struct members can be rearranged to not wasted memory
      due to padding. For example. when an odd number of int is followed by a
      long. This has also been done in a preciously thesis projects and it gained
      some memory [2].


    ***************************************************************************************************************
    ***************************************************************************************************************
    ***************************************************************************************************************
*/


namespace FMOD
{


FMOD_CODEC_DESCRIPTION_EX tremorcodec;

#ifdef PLUGIN_EXPORTS

#ifdef __cplusplus
extern "C" {
#endif

    /*
        FMODGetCodecDescription is mandantory for every fmod plugin.  This is the symbol the registerplugin function searches for.
        Must be declared with F_API to make it export as stdcall.
    */
    F_DECLSPEC F_DLLEXPORT FMOD_CODEC_DESCRIPTION_EX * F_API FMODGetCodecDescriptionEx()
    {
        return CodecTremor::getDescriptionEx();
    }

#ifdef __cplusplus
}
#endif

#endif  /* PLUGIN_EXPORTS */

/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

    [PLATFORMS]
    Win32, Win64, Linux, Macintosh, XBox, PlayStation 2, GameCube

	[SEE_ALSO]
]
*/
size_t FMOD_Tremor_ReadCallback(void *ptr, size_t size, size_t nmemb, void *datasource)
{
    FMOD_RESULT result;
    unsigned int rd;
	File *fp;
    
    fp = (File *)datasource;
	
    result = fp->read(ptr, (unsigned int)size, (unsigned int)nmemb, &rd);
	if (result != FMOD_OK && result != FMOD_ERR_FILE_EOF)
    {
        return (size_t)-1;
    }

	return rd;
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

    [PLATFORMS]
    Win32, Win64, Linux, Macintosh, XBox, PlayStation 2, GameCube

	[SEE_ALSO]
]
*/
int FMOD_Tremor_SeekCallback(void *datasource, ogg_int64_t offset, int whence)
{
	File *fp = (File *)datasource;

    if (fp->mFlags & FMOD_FILE_SEEKABLE)
    {
	    return fp->seek((int)offset, (signed char)whence);
    }
    else
    {
        return -1;
    }
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

    [PLATFORMS]
    Win32, Win64, Linux, Macintosh, XBox, PlayStation 2, GameCube

	[SEE_ALSO]
]
*/
int FMOD_Tremor_TellCallback(void *datasource)
{
	File *fp = (File *)datasource;
    unsigned int pos;

    fp->tell(&pos);

	return (ogg_int32_t)pos;
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

    [PLATFORMS]
    Win32, Win64, Linux, Macintosh, XBox, PlayStation 2, GameCube

	[SEE_ALSO]
]
*/
void * FMOD_Tremor_Malloc(int size)
{
    return FMOD_Memory_Alloc(size);
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

    [PLATFORMS]
    Win32, Win64, Linux, Macintosh, XBox, PlayStation 2, GameCube

	[SEE_ALSO]
]
*/
void * FMOD_Tremor_Calloc(int count, int size)
{
    return FMOD_Memory_Calloc(count * size);
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

    [PLATFORMS]
    Win32, Win64, Linux, Macintosh, XBox, PlayStation 2, GameCube

	[SEE_ALSO]
]
*/
void * FMOD_Tremor_ReAlloc(void *ptr, int size)
{
    return FMOD_Memory_ReAlloc(ptr, size);
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

    [PLATFORMS]
    Win32, Win64, Linux, Macintosh, XBox, PlayStation 2, GameCube

	[SEE_ALSO]
]
*/
void   FMOD_Tremor_Free(void *ptr)
{
    FMOD_Memory_Free(ptr);
}

extern "C"
{
    void * (*_ogg_malloc)  (int size)            = FMOD_Tremor_Malloc;
    void * (*_ogg_calloc)  (int count, int size) = FMOD_Tremor_Calloc;
    void * (*_ogg_realloc) (void *ptr, int size) = FMOD_Tremor_ReAlloc;
    void   (*_ogg_free)    (void *ptr)           = FMOD_Tremor_Free;
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

    [PLATFORMS]
    Win32, Win64, Linux, Macintosh, XBox, PlayStation 2, GameCube

	[SEE_ALSO]
]
*/
FMOD_CODEC_DESCRIPTION_EX *CodecTremor::getDescriptionEx()
{
    memset(&tremorcodec, 0, sizeof(FMOD_CODEC_DESCRIPTION_EX));

    tremorcodec.name        = "FMOD Tremor Codec";
    tremorcodec.version     = 0x00010100;
    tremorcodec.timeunits   = FMOD_TIMEUNIT_PCM;
    tremorcodec.open        = &CodecTremor::openCallback;
    tremorcodec.close       = &CodecTremor::closeCallback;
    tremorcodec.read        = &CodecTremor::readCallback;
    tremorcodec.setposition = &CodecTremor::setPositionCallback;

    tremorcodec.mType       = FMOD_SOUND_TYPE_OGGVORBIS;
    tremorcodec.mSize       = sizeof(CodecTremor);

    return &tremorcodec;
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

    [PLATFORMS]
    Win32, Win64, Linux, Macintosh, XBox, PlayStation 2, GameCube

	[SEE_ALSO]
]
*/
FMOD_RESULT CodecTremor::openInternal(FMOD_MODE usermode, FMOD_CREATESOUNDEXINFO *userexinfo)
{
    FMOD_RESULT     result = FMOD_OK;
    vorbis_info     *vi;
    char            str[4];
    unsigned int    lengthbytes = 0;
    int             oggresult;
	ov_callbacks callbacks = 
	{
		FMOD_Tremor_ReadCallback, /* size_t (*read_func)  (void *ptr, size_t size, size_t nmemb, void *datasource); */
		FMOD_Tremor_SeekCallback, /* int    (*seek_func)  (void *datasource, ogg_int64_t offset, int whence); */
		0,                        /* int    (*close_func) (void *datasource); */
		FMOD_Tremor_TellCallback, /* int    (*tell_func)  (void *datasource); */
	};
    bool manualsizecalc = false;

    init(FMOD_SOUND_TYPE_OGGVORBIS);

    FLOG((FMOD_DEBUG_LEVEL_LOG, __FILE__, __LINE__, "CodecTremor::openInternal", "attempting to open as OGG..\n"));

	result = mFile->seek(0, SEEK_SET);
    if (result != FMOD_OK)
    {
        return result;
    }

	mSrcDataOffset = 0;

	/*
        Support RIFF wrapped MP3?
    */
    {
        CodecWav tempwav;
        WAVE_CHUNK chunk;
        FMOD_CODEC_WAVEFORMAT tempwaveformat;
        
        memset(&tempwav, 0, sizeof(CodecWav));
        memset(&tempwaveformat, 0, sizeof(FMOD_CODEC_WAVEFORMAT));

        tempwav.mFile = mFile;
        tempwav.mSrcDataOffset = (unsigned int)-1;
        tempwav.waveformat = &tempwaveformat;
	    
	    /*
            Read header
        */
        result = mFile->read(&chunk, 1, sizeof(WAVE_CHUNK), 0);
        if (result != FMOD_OK)
        {
            return result;
        }

        if (!FMOD_strncmp((const char *)chunk.id, "RIFF", 4))
        {
            char wave[4];
            
            result = mFile->read(wave, 1, 4, 0);
            if (result != FMOD_OK)
            {
                return result;
            }

            if (!FMOD_strncmp(wave, "WAVE", 4))
            {
                #ifdef PLATFORM_ENDIAN_BIG
                chunk.size = FMOD_SWAPENDIAN_DWORD(chunk.size);
                #endif
            
                result = tempwav.parseChunk(chunk.size);
                if (result == FMOD_OK && tempwav.mSrcFormat && tempwav.mSrcDataOffset == (unsigned int)-1)
                {
                    int format = tempwav.mSrcFormat->Format.wFormatTag;

                    if (format == WAVE_FORMAT_OGGVORBIS)
                    {
                        mSrcDataOffset          = tempwav.mSrcDataOffset;
                        lengthbytes             = tempwav.waveformat[0].lengthbytes;
                        mLoopPoints[0]          = tempwav.mLoopPoints[0];
                        mLoopPoints[1]          = tempwav.mLoopPoints[1];
                        mSyncPoint              = tempwav.mSyncPoint;
                        mNumSyncPoints          = tempwav.mNumSyncPoints;
                    }
                    else
                    {
    		            result = FMOD_ERR_FORMAT;
                    }

                    if (result != FMOD_OK)
                    {
	                    if (tempwav.mSrcFormat)
                        {
		                    FMOD_Memory_Free(tempwav.mSrcFormat);
                            tempwav.mSrcFormat = 0;
                        }  
                        if (tempwav.mSyncPoint && mSyncPoint != tempwav.mSyncPoint)
                        {
		                    FMOD_Memory_Free(tempwav.mSyncPoint);
                            tempwav.mSyncPoint = 0;
                        }
                        return result;
                    }
                }

	            if (tempwav.mSrcFormat)
                {
		            FMOD_Memory_Free(tempwav.mSrcFormat);
                    tempwav.mSrcFormat = 0;
                }                 
                if (tempwav.mSyncPoint && mSyncPoint != tempwav.mSyncPoint)
                {
                    FMOD_Memory_Free(tempwav.mSyncPoint);
                    tempwav.mSyncPoint = 0;
                }
            }
        }

        result = mFile->seek(mSrcDataOffset, SEEK_SET);
    }

    result = mFile->read(str, 1, 4, 0);
    if (result != FMOD_OK)
    {
        return result;
    }

	if (FMOD_strncmp(str, "OggS", 4))
	{
        FLOG((FMOD_DEBUG_LEVEL_LOG, __FILE__, __LINE__, "CodecTremor::openInternal", "failed to open as ogg\n"));

		return FMOD_ERR_FORMAT;
	}

	/*
        Get size of ogg file in bytes
    */
	/*
        If there wasnt a riff size chunk
    */
	if (!lengthbytes)
	{
	    result = mFile->getSize(&lengthbytes);
        if (result != FMOD_OK)
        {
            return result;
        }

        manualsizecalc = true;
    }

	result = mFile->seek(mSrcDataOffset, SEEK_SET);
    if (result != FMOD_OK)
    {
        return result;
    }

    oggresult = ov_open_callbacks(mFile, &mVf, 0, 0, callbacks);
	if (oggresult < 0)
	{
        FLOG((FMOD_DEBUG_LEVEL_LOG, __FILE__, __LINE__, "CodecTremor::openInternal", "failed to open as ogg\n"));

        if (oggresult == -139) //OV_EMEMORY)
        {
            return FMOD_ERR_MEMORY;
        }
		return FMOD_ERR_FORMAT;
	}

	vi = ov_info(&mVf,-1);

    result = readVorbisComments();
    if (result != FMOD_OK)
    {
        return result;
    }

    waveformat = &mWaveFormat;
    waveformat[0].lengthbytes = lengthbytes;
    waveformat[0].format     = FMOD_SOUND_FORMAT_PCM16;
	waveformat[0].channels   = vi->channels;
    waveformat[0].frequency  = vi->rate;
    waveformat[0].blockalign = waveformat[0].channels * 2;                    /* THIS SHOULD BE EQUIVALENT TO THE OGG DECODE BUFFER SIZE? */
 
    if (manualsizecalc && waveformat[0].lengthbytes != (unsigned int)-1)
    {
        waveformat[0].lengthbytes -= mSrcDataOffset;
    }

    if (mFile->mFlags & FMOD_FILE_SEEKABLE)
    {
        int count, streams = ov_streams(&mVf);

        waveformat[0].lengthpcm = 0;

        for (count = 0; count < streams; count++)
        {
	        waveformat[0].lengthpcm += (int)ov_pcm_total(&mVf, count);
        }

	    if (waveformat[0].lengthpcm <= 0)
	    {
            FLOG((FMOD_DEBUG_LEVEL_LOG, __FILE__, __LINE__, "CodecTremor::openInternal", "failed to open as ogg\n"));
    
            waveformat[0].lengthpcm = 0;

		    return FMOD_ERR_FORMAT;
	    }
    }
    else
    {
        waveformat[0].lengthpcm = 0x7fffffff;
    }

    if (!mSrcDataOffset)
    {
	    mSrcDataOffset = (unsigned int)ov_raw_tell(&mVf);
    }

    /*
        Fill out base class members, also pointing to or allocating storage for them.
    */
    numsubsounds = 0;

    return result;
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

    [PLATFORMS]
    Win32, Win64, Linux, Macintosh, XBox, PlayStation 2, GameCube

	[SEE_ALSO]
]
*/
FMOD_RESULT CodecTremor::closeInternal()
{       
    mVf.datasource = 0;		/* this stops vorbis from trying to close our file */

    ov_clear(&mVf);
    
    return FMOD_OK;
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

    [PLATFORMS]
    Win32, Win64, Linux, Macintosh, XBox, PlayStation 2, GameCube

	[SEE_ALSO]
]
*/
FMOD_RESULT CodecTremor::readInternal(void *buffer, unsigned int sizebytes, unsigned int *bytesread)
{
    int bigendian;
    vorbis_comment *vc;
    char *name, *value;
    int i;

#ifdef PLATFORM_ENDIAN_BIG
    bigendian = 1;
#else
    bigendian = 0;
#endif

    *bytesread = ov_read(&mVf, buffer, sizebytes, 0);
    if (!*bytesread)
    {
        return FMOD_ERR_FILE_EOF;
    }

    vc  = ov_comment(&mVf, -1);

    if (vc && vc->comments)
    {
        for (i=0;i < vc->comments;i++)
        {
            name = vc->user_comments[i];
            value = name;
            for (;*value && (*value != '=');value++) ;

            if (*value == '=')
            {
                *value++ = 0;
            }
            else
            {
                value = name;
                name  = "NONAME";
            }

            metadata((FMOD_CODEC_STATE *)this, FMOD_TAGTYPE_VORBISCOMMENT, name, value, FMOD_strlen(value) + 1, FMOD_TAGDATATYPE_STRING, true);
        }

#ifndef FMOD_MODIFIED
        vorbis_comment_clear(vc);
#endif
    }

    return FMOD_OK;	
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

    [PLATFORMS]
    Win32, Win64, Linux, Macintosh, XBox, PlayStation 2, GameCube

	[SEE_ALSO]
]
*/
FMOD_RESULT CodecTremor::setPositionInternal(int subsound, unsigned int position, FMOD_TIMEUNIT postype)
{
    FMOD_RESULT result = FMOD_OK;

    if (ov_pcm_seek(&mVf, position) < 0)
    {
        return FMOD_ERR_MEMORY;
    }

    return result;
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

	[SEE_ALSO]
]
*/
FMOD_RESULT CodecTremor::readVorbisComments()
{
    FMOD_RESULT result;
    char *p;
    int count, i;
    vorbis_comment *vc;

    vc = ov_comment(&mVf, -1);
    if (!vc)
    {
        return FMOD_OK;
    }

    for (count = 0; count < vc->comments; count++)
    {
        if (vc->comment_lengths[count])
        {
            p = vc->user_comments[count];
            for (i=0;*p && (*p != '=');i++, p++) ;
            if (*p == '=')
            {
                *p++ = 0;
                result = metaData(FMOD_TAGTYPE_VORBISCOMMENT, vc->user_comments[count], p, FMOD_strlen(p) + 1, FMOD_TAGDATATYPE_STRING, false);
                if (result != FMOD_OK)
                {
                    return result;
                }
            }
        }
    }

    return FMOD_OK;
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

    [PLATFORMS]
    Win32, Win64, Linux, Macintosh, XBox, PlayStation 2, GameCube

	[SEE_ALSO]
]
*/
FMOD_RESULT F_CALLBACK CodecTremor::openCallback(FMOD_CODEC_STATE *codec, FMOD_MODE usermode, FMOD_CREATESOUNDEXINFO *userexinfo)
{
    CodecTremor *ogg = (CodecTremor *)codec;

    return ogg->openInternal(usermode, userexinfo);
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

    [PLATFORMS]
    Win32, Win64, Linux, Macintosh, XBox, PlayStation 2, GameCube

	[SEE_ALSO]
]
*/
FMOD_RESULT F_CALLBACK CodecTremor::closeCallback(FMOD_CODEC_STATE *codec)
{
    CodecTremor *ogg = (CodecTremor *)codec;

    return ogg->closeInternal();
}


/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

    [PLATFORMS]
    Win32, Win64, Linux, Macintosh, XBox, PlayStation 2, GameCube

	[SEE_ALSO]
]
*/
FMOD_RESULT F_CALLBACK CodecTremor::readCallback(FMOD_CODEC_STATE *codec, void *buffer, unsigned int sizebytes, unsigned int *bytesread)
{
    CodecTremor *ogg = (CodecTremor *)codec;

    return ogg->readInternal(buffer, sizebytes, bytesread);
}

/*
[
	[DESCRIPTION]

	[PARAMETERS]
 
	[RETURN_VALUE]

	[REMARKS]

    [PLATFORMS]
    Win32, Win64, Linux, Macintosh, XBox, PlayStation 2, GameCube

	[SEE_ALSO]
]
*/
FMOD_RESULT F_CALLBACK CodecTremor::setPositionCallback(FMOD_CODEC_STATE *codec, int subsound, unsigned int position, FMOD_TIMEUNIT postype)
{
    CodecTremor *ogg = (CodecTremor *)codec;

    return ogg->setPositionInternal(subsound, position, postype);
}

}

#endif


