/*
 * miniJSS - Mixing device and channel handling
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2006-2012 Tecnic Software productions (TNSP)
 */
#include "jssmix.h"
#include <string.h>


#ifdef DM_USE_C
#define JMIXER_HEADER
#include "jmix_c_in.c"
#undef JMIXER_HEADER
#endif

#undef DM_USE_SIMD

#ifdef DM_USE_SIMD
#define JMIXER_HEADER
#include "jmix_mmx_in.c"
#undef JMIXER_HEADER
#endif


typedef struct
{
    int    mixerID;
    int    outFormat;
    int    outChannels;

    int    (*jvmMixChannel_FW)(JSSMixer *, JSSChannel *, JMIXER_ADDBUF_TYPE *, const int, const DMFixedPoint);
    int    (*jvmMixChannel_BW)(JSSMixer *, JSSChannel *, JMIXER_ADDBUF_TYPE *, const int, const DMFixedPoint);
    void   (*jvmPostProcess)(JMIXER_ADDBUF_TYPE *, void *, const int);
} JSSMixingRoutine;


/* This table should be sorted from fastest to slowest, e.g. MMX/x86
 * optimized routines first, pure C versions last.
 */
static JSSMixingRoutine jvmMixRoutines[] =
{
#ifdef DM_USE_SIMD
{ JMIX_MMX, JSS_AUDIO_U8,  JSS_AUDIO_MONO,   jvmMix_Mono_MMX_FW, jvmMix_Mono_MMX_BW, jvmPostProcess_U8_MMX },
{ JMIX_MMX, JSS_AUDIO_S8,  JSS_AUDIO_MONO,   jvmMix_Mono_MMX_FW, jvmMix_Mono_MMX_BW, jvmPostProcess_S8_MMX },
{ JMIX_MMX, JSS_AUDIO_U8,  JSS_AUDIO_STEREO, jvmMix_Stereo_MMX_FW, jvmMix_Stereo_MMX_BW, jvmPostProcess_U8_MMX },
{ JMIX_MMX, JSS_AUDIO_S8,  JSS_AUDIO_STEREO, jvmMix_Stereo_MMX_FW, jvmMix_Stereo_MMX_BW, jvmPostProcess_S8_MMX },

{ JMIX_MMX, JSS_AUDIO_U16, JSS_AUDIO_MONO,   jvmMix_Mono_MMX_FW, jvmMix_Mono_MMX_BW, jvmPostProcess_U16_MMX },
{ JMIX_MMX, JSS_AUDIO_S16, JSS_AUDIO_MONO,   jvmMix_Mono_MMX_FW, jvmMix_Mono_MMX_BW, jvmPostProcess_S16_MMX },
{ JMIX_MMX, JSS_AUDIO_U16, JSS_AUDIO_STEREO, jvmMix_Stereo_MMX_FW, jvmMix_Stereo_MMX_BW, jvmPostProcess_U16_MMX },
{ JMIX_MMX, JSS_AUDIO_S16, JSS_AUDIO_STEREO, jvmMix_Stereo_MMX_FW, jvmMix_Stereo_MMX_BW, jvmPostProcess_S16_MMX },
#endif

#ifdef DM_USE_C
{ JMIX_C,   JSS_AUDIO_U8,  JSS_AUDIO_MONO,   jvmMix_Mono_C_FW, jvmMix_Mono_C_BW, jvmPostProcess_U8_C },
{ JMIX_C,   JSS_AUDIO_S8,  JSS_AUDIO_MONO,   jvmMix_Mono_C_FW, jvmMix_Mono_C_BW, jvmPostProcess_S8_C },
{ JMIX_C,   JSS_AUDIO_U8,  JSS_AUDIO_STEREO, jvmMix_Stereo_C_FW, jvmMix_Stereo_C_BW, jvmPostProcess_U8_C },
{ JMIX_C,   JSS_AUDIO_S8,  JSS_AUDIO_STEREO, jvmMix_Stereo_C_FW, jvmMix_Stereo_C_BW, jvmPostProcess_S8_C },

{ JMIX_C,   JSS_AUDIO_U16, JSS_AUDIO_MONO,   jvmMix_Mono_C_FW, jvmMix_Mono_C_BW, jvmPostProcess_U16_C },
{ JMIX_C,   JSS_AUDIO_S16, JSS_AUDIO_MONO,   jvmMix_Mono_C_FW, jvmMix_Mono_C_BW, jvmPostProcess_S16_C },
{ JMIX_C,   JSS_AUDIO_U16, JSS_AUDIO_STEREO, jvmMix_Stereo_C_FW, jvmMix_Stereo_C_BW, jvmPostProcess_U16_C },
{ JMIX_C,   JSS_AUDIO_S16, JSS_AUDIO_STEREO, jvmMix_Stereo_C_FW, jvmMix_Stereo_C_BW, jvmPostProcess_S16_C },
#endif
};

static const int jvmNMixRoutines = sizeof(jvmMixRoutines) / sizeof(jvmMixRoutines[0]);


static int jvmFindMixRoutine(int outFormat, int outChannels, int mixerID)
{
    int i;
    
    for (i = 0; i < jvmNMixRoutines; i++)
    {
        if (jvmMixRoutines[i].outFormat == outFormat &&
            jvmMixRoutines[i].outChannels == outChannels &&
            (mixerID == JMIX_AUTO || jvmMixRoutines[i].mixerID == mixerID))
            return i;
    }
    
    return -1;
}


JSSMixer *jvmInit(const int outFormat, const int outChannels, const int outFreq, const int mixerID)
{
    JSSMixer *mixer;
    int mixerIdx;

    // Check settings
    if (outChannels < 1)
    {
        JSSERROR(DMERR_INVALID_ARGS, NULL,
        "Invalid number of channels %d\n", outChannels);
    }
    
    if (outFreq < 4000)
    {
        JSSERROR(DMERR_INVALID_ARGS, NULL,
        "Invalid mixing frequency %d\n", outFreq);
    }
    
    /* Select mixing routines:
     * Here we try to choose the most fitting mixing routines
     * from the compiled in routines, unless caller is forcing
     * us to select specific ones.
     */
    if (mixerID == JMIX_AUTO)
    {
        mixerIdx = jvmFindMixRoutine(outFormat, outChannels, JMIX_SSE);
        if (mixerIdx < 0)
            mixerIdx = jvmFindMixRoutine(outFormat, outChannels, JMIX_MMX);
        if (mixerIdx < 0)
            mixerIdx = jvmFindMixRoutine(outFormat, outChannels, JMIX_AUTO);
    }
    else
    {
        mixerIdx = jvmFindMixRoutine(outFormat, outChannels, mixerID);
    }

    if (mixerIdx < 0)
    {
        JSSERROR(DMERR_INVALID_ARGS, NULL,
        "Could not find mixing routine for outFormat=%d, outChannels=%d, outFreq=%d.\n",
        outFormat, outChannels, outFreq);
        return NULL;
    }
    
    // Allocate a mixer device structure
    mixer = dmMalloc0(sizeof(JSSMixer));
    if (mixer == NULL)
    {
        JSSERROR(DMERR_MALLOC, NULL,
        "Could not allocate mixing device structure.\n");
    }

    // Initialize variables
#ifdef JSS_SUP_THREADS
    mixer->mutex = dmCreateMutex();
#endif
    mixer->outFormat = outFormat;
    mixer->outFreq = outFreq;
    mixer->outChannels = outChannels;
    
    mixer->jvmMixChannel_FW = jvmMixRoutines[mixerIdx].jvmMixChannel_FW;
    mixer->jvmMixChannel_BW = jvmMixRoutines[mixerIdx].jvmMixChannel_BW;
    mixer->jvmPostProcess = jvmMixRoutines[mixerIdx].jvmPostProcess;
    
    // Allocate addBuffer
    mixer->addBufSize = outChannels * outFreq * 2;
    mixer->addBuffer = dmMalloc(mixer->addBufSize * sizeof(JMIXER_ADDBUF_TYPE));
    if (mixer->addBuffer == NULL)
    {
        JSSERROR(DMERR_MALLOC, NULL,
        "Could not allocate mixing addition buffer.\n");
    }
    
    return mixer;
}


int jvmClose(JSSMixer * mixer)
{
    if (mixer == NULL)
        return DMERR_NULLPTR;

    // Deallocate resources
#ifdef JSS_SUP_THREADS
    dmDestroyMutex(mixer->mutex);
#endif
    dmFree(mixer->addBuffer);

    memset(mixer, 0, sizeof(JSSMixer));
    dmFree(mixer);

    return DMERR_OK;
}


int jvmGetSampleSize(JSSMixer *mixer)
{
    int sampSize = 1;
    assert(mixer);
    
    switch (mixer->outChannels)
    {
        case JSS_AUDIO_STEREO:
        case JSS_AUDIO_MONO:
            sampSize = mixer->outChannels;
            break;
        default:
            JSSERROR(DMERR_INVALID_ARGS, -1,
            "outChannels=%d not stereo or mono!\n", mixer->outChannels);
            break;
    }
    
    switch (mixer->outFormat)
    {
        case JSS_AUDIO_U16: sampSize *= sizeof(Uint16); break;
        case JSS_AUDIO_S16: sampSize *= sizeof(Sint16); break;
        case JSS_AUDIO_U8:  sampSize *= sizeof(Uint8); break;
        case JSS_AUDIO_S8:  sampSize *= sizeof(Sint8); break;
        default:
            JSSERROR(DMERR_INVALID_ARGS, -1,
            "outFormat=%d is not supported!\n", mixer->outFormat);
    }
    
    return sampSize;
}


int jvmGetSampleRes(JSSMixer *mixer)
{
    int sampRes = 0;
    
    assert(mixer);
    
    switch (mixer->outFormat)
    {
        case JSS_AUDIO_U16: case JSS_AUDIO_S16: sampRes = 16; break;
        case JSS_AUDIO_U8:  case JSS_AUDIO_S8:  sampRes = 8; break;
        default:
            JSSERROR(DMERR_INVALID_ARGS, -1,
            "outFormat=%d is not supported!\n", mixer->outFormat);
    }
    
    return sampRes;
}


static void jvmMixChannel(JSSMixer *mixer, JSSChannel *chn, JMIXER_ADDBUF_TYPE *addBuffer, const int mixLength)
{
    int mixDone = mixLength, mixResult;
    JMIXER_ADDBUF_TYPE *ab = addBuffer;
    
    if (!chn->chPlaying || chn->chMute)
        return;

    DBG("%s(%p, %d)\n", __FUNCTION__, chn, mixLength);
    
    while (mixDone > 0)
    {
        if (chn->chDirection)
        {
            // Channel is playing FORWARDS
            if (chn->chFlags & jsfLooped)
            {
                // Sample is looped
                if (chn->chFlags & jsfBiDi)
                {
                    // Bi-directional loop
                    if (chn->chPos.dw >= chn->chLoopE.dw)
                    {
                        DMFixedPoint end;
                        FP_ADD_R(end, chn->chLoopE, chn->chLoopE);
                        FP_SUB_R(chn->chPos, end, chn->chPos);
                        chn->chDirection = FALSE;
                    }
                }
                else
                {
                    // Normal forward loop
                    if (chn->chPos.dw >= chn->chLoopE.dw)
                    {
                        DMFixedPoint diff;
                        FP_SUB_R(diff, chn->chPos, chn->chLoopE);
                        FP_ADD_R(chn->chPos, chn->chLoopS, diff);
                    }
                }
            }
            else
            {
                // Normal (non-looped) sample
                if (chn->chPos.dw >= chn->chSize.dw)
                {
                    chn->chPlaying = FALSE;
                    return;
                }
            }
        }
        else
        {
            // Channel is playing BACKWARDS
            if (chn->chFlags & jsfLooped)
            {
                // Sample is looped
                if (chn->chFlags & jsfBiDi)
                {
                    // Bi-directional loop
                    if (chn->chPos.dw <= chn->chLoopS.dw)
                    {
                        DMFixedPoint start;
                        FP_ADD_R(start, chn->chLoopS, chn->chLoopS);
                        FP_SUB_R(chn->chPos, start, chn->chPos);
                        chn->chDirection = TRUE;
                    }
                }
                else
                {
                    // Normal forward loop
                    if (chn->chPos.dw <= chn->chLoopS.dw)
                    {
                        DMFixedPoint diff;
                        FP_SUB_R(diff, chn->chLoopE, chn->chLoopS);
                        FP_ADD(chn->chPos, diff);
                    }
                }
            }
            else
            {
                // Normal (non-looped) sample
                if (chn->chPos.dw <= 0)
                {
                    chn->chPlaying = FALSE;
                    return;
                }
            }
        }
        
        // Call the mixing innerloop functions
        if (chn->chDirection)
        {
            DBG("MIX_FW[%p : %d : ", ab, mixDone);
            if (chn->chFlags & jsfLooped)
            {
                DBG("%d (%x)] {loop}\n", FP_GETH(chn->chLoopE), FP_GETH(chn->chLoopE));
                mixResult = mixer->jvmMixChannel_FW((void *) mixer, chn,
                    ab, mixDone, chn->chLoopE);
            }
            else
            {
                DBG("%d (%x)]\n", FP_GETH(chn->chSize), FP_GETH(chn->chSize));
                mixResult = mixer->jvmMixChannel_FW((void *) mixer, chn,
                    ab, mixDone, chn->chSize);
            }
        }
        else
        {
            DBG("MIX_BW[%p : %d : ", ab, mixDone);
            if (chn->chFlags & jsfLooped)
            {
                DBG("%d (%x)] {loop}\n", chn->chLoopS, chn->chLoopS);
                mixResult = mixer->jvmMixChannel_BW(mixer, chn,
                    ab, mixDone, chn->chLoopS);
            }
            else
            {
                static const DMFixedPoint zero = { 0 };
                DBG("%d (%x)]\n", 0, 0);
                mixResult = mixer->jvmMixChannel_BW(mixer, chn,
                    ab, mixDone, zero);
            }
        }
        
        mixDone -= mixResult;
        ab += mixResult * mixer->outChannels;
    }

#ifdef JSS_DEBUG
    if (mixDone < 0)
        JSSWARNING(DMERR_BOUNDS,, "mixDone < 0 in mixing logic loop.\n");
#endif
}


void jvmRenderAudio(JSSMixer *mixer, void *mixBuffer, const int mixLength)
{
    int i, blockLength, mixLeft;
    JMIXER_ADDBUF_TYPE *ab;

    JSS_LOCK(mixer);

    assert(mixer != NULL);
    assert(mixBuffer != NULL);
    assert(mixLength > 0);
    assert(mixLength * mixer->outChannels <= mixer->addBufSize);

    // Clear mixer->addBuffer
    memset(mixer->addBuffer, 0, mixLength * mixer->outChannels * sizeof(JMIXER_ADDBUF_TYPE));
    
    ab = mixer->addBuffer;
    mixLeft = mixLength;
    while (mixLeft > 0)
    {
        // Check for callbacks
        blockLength = mixLeft;

        if (mixer->cbFunction)
        {
            if (mixer->cbCounter <= 0)
            {
                mixer->cbFunction(mixer, mixer->cbData);
                mixer->cbCounter = mixer->cbFreq;
            }

            if (mixer->cbCounter < blockLength)
                blockLength = mixer->cbCounter;
        }

        // Do mixing
        for (i = 0; i < jsetNChannels; i++)
        {
            JSSChannel *chn = &(mixer->channels[i]);
            if (chn->chPlaying && !chn->chMute)
                jvmMixChannel(mixer, chn, ab, blockLength);
        }

/*
            if (chn->chPlaying)
            {
                if (!chn->chMute)
                    jvmMixChannel(mixer, chn, ab, blockLength);
                else
                    jvmAdvanceChannel(mixer, chn, blockLength);
            }
*/
        
        ab += blockLength * mixer->outChannels;
        mixLeft -= blockLength;
        mixer->cbCounter -= blockLength;
    }

    // Post-process
    mixer->jvmPostProcess(mixer->addBuffer, mixBuffer, mixLength * mixer->outChannels);

    JSS_UNLOCK(mixer);
}


int jvmSetCallback(JSSMixer * mixer, void (*cbFunction) (void *, void *), void *cbData)
{
    assert(mixer);

    if (cbFunction == NULL)
        JSSERROR(DMERR_NULLPTR, DMERR_NULLPTR, "NULL pointer given as cbFunction");

    JSS_LOCK(mixer);

    mixer->cbFunction = cbFunction;
    mixer->cbData = cbData;

    JSS_UNLOCK(mixer);

    return DMERR_OK;
}


void jvmRemoveCallback(JSSMixer * mixer)
{
    assert(mixer);

    JSS_LOCK(mixer);

    mixer->cbFunction = NULL;
    mixer->cbData = NULL;
    mixer->cbFreq = mixer->cbCounter = 0;

    JSS_UNLOCK(mixer);
}


int jvmSetCallbackFreq(JSSMixer * mixer, const int cbFreq)
{
    assert(mixer);

    JSS_LOCK(mixer);
    
    mixer->cbFreq = cbFreq;
    mixer->cbCounter = 0;

//fprintf(stderr, "set(outFreq = %d, cbFreq = %d) = %d\n", mixer->outFreq, cbFreq, mixer->cbFreq);
    
    JSS_UNLOCK(mixer);
    return DMERR_OK;
}


/* Channel manipulation routines
 */
void jvmPlay(JSSMixer * mixer, const int channel)
{
    JSS_LOCK(mixer);
    mixer->channels[channel].chPlaying = TRUE;
    JSS_UNLOCK(mixer);
}


void jvmStop(JSSMixer * mixer, const int channel)
{
    JSS_LOCK(mixer);
    mixer->channels[channel].chPlaying = FALSE;
    JSS_UNLOCK(mixer);
}


void jvmReset(JSSMixer * mixer, const int channel)
{
    JSSChannel *c;
    
    JSS_LOCK(mixer);
    c = &mixer->channels[channel];

    c->chDirection = TRUE;
    c->chPos.dw    = c->chDeltaO.dw = 0;

    JSS_UNLOCK(mixer);
}


void jvmSetSample(JSSMixer * mixer, const int channel,
          void *data, const Sint32 size, const Sint32 loopS,
          const Sint32 loopE, const int flags)
{
    JSSChannel *c;
    
    JSS_LOCK(mixer);
    c = &mixer->channels[channel];
    
    FP_SETHL(c->chSize, size, 0);
    FP_SETHL(c->chLoopS, loopS, 0);
    FP_SETHL(c->chLoopE, loopE, 0);
    c->chData      = data;
    c->chFlags     = flags;
    c->chDirection = TRUE;
    c->chPos.dw    = c->chDeltaO.dw = 0;
    
    JSS_UNLOCK(mixer);
}


void jvmSetFreq(JSSMixer * mixer, const int channel, const int freq)
{
    JSS_LOCK(mixer);

    mixer->channels[channel].chFreq = freq;
    
    if (mixer->outFreq > 0)
    {
        DMFixedPoint a, b;
        FP_SETHL(a, freq, 0);
        FP_CONV(b, mixer->outFreq);
        FP_DIV_R(mixer->channels[channel].chDeltaO, a, b);
    }
    else
    {
        FP_SET(mixer->channels[channel].chDeltaO, 0);
    }

    JSS_UNLOCK(mixer);
}


int jvmGetFreq(JSSMixer * mixer, const int channel)
{
    int tmp;

    JSS_LOCK(mixer);
    tmp = mixer->channels[channel].chFreq;
    JSS_UNLOCK(mixer);

    return tmp;
}


void jvmSetVolume(JSSMixer * mixer, const int channel, const int volume)
{
    JSS_LOCK(mixer);
    FP_SETHL(mixer->channels[channel].chVolume, volume, 0);
    mixer->channels[channel].chVolumeD = 0;
    mixer->channels[channel].chDeltaV.dw = 0;
    JSS_UNLOCK(mixer);
}


void jvmSetVolumeRamp(JSSMixer * mixer, const int channel, const int start, const int end, const int len)
{
    int tmp;
    DMFixedPoint a, b;
    JSS_LOCK(mixer);
    FP_SETHL(mixer->channels[channel].chVolume, start, 0);

    tmp = mixer->channels[channel].chVolumeD = 
        len > 0 ? ((mixer->outFreq * len) / 1000) : mixer->cbFreq;

    FP_SETHL(a, (end - start), 0);
    FP_CONV(b, tmp);
    FP_DIV_R(mixer->channels[channel].chDeltaV, a, b);

    JSS_UNLOCK(mixer);
}


int jvmGetVolume(JSSMixer * mixer, const int channel)
{
    int tmp;

    JSS_LOCK(mixer);
    tmp = FP_GETH(mixer->channels[channel].chVolume);
    JSS_UNLOCK(mixer);

    return tmp;
}


void jvmSetPos(JSSMixer * mixer, const int channel, const Sint32 pos)
{
    JSS_LOCK(mixer);
    FP_SETHL(mixer->channels[channel].chPos, pos, 0);
    JSS_UNLOCK(mixer);
}


Sint32 jvmGetPos(JSSMixer * mixer, const int channel)
{
    Sint32 tmp;

    JSS_LOCK(mixer);
    tmp = FP_GETH(mixer->channels[channel].chPos);
    JSS_UNLOCK(mixer);

    return tmp;
}


void jvmSetPan(JSSMixer * mixer, const int channel, const int panning)
{
    JSS_LOCK(mixer);
    FP_SETHL(mixer->channels[channel].chPanning, panning, 0);
    mixer->channels[channel].chPanningD = 0;
    mixer->channels[channel].chDeltaP.dw = 0;
    JSS_UNLOCK(mixer);
}


void jvmSetPanRamp(JSSMixer * mixer, const int channel, const int start, const int end, const int len)
{
    int tmp;
    DMFixedPoint a, b;
    JSS_LOCK(mixer);

    FP_SETHL(mixer->channels[channel].chPanning, start, 0);

    tmp = mixer->channels[channel].chPanningD = 
        len > 0 ? ((mixer->outFreq * len) / 1000) : mixer->cbFreq;

    FP_SETHL(a, (end - start), 0);
    FP_CONV(b, tmp);
    FP_DIV_R(mixer->channels[channel].chDeltaP, a, b);

    JSS_UNLOCK(mixer);
}


int jvmGetPan(JSSMixer * mixer, const int channel)
{
    int tmp;

    JSS_LOCK(mixer);
    tmp = FP_GETH(mixer->channels[channel].chPanning);
    JSS_UNLOCK(mixer);

    return tmp;
}


void jvmMute(JSSMixer * mixer, const int channel, const BOOL mute)
{
    JSS_LOCK(mixer);
    mixer->channels[channel].chMute = mute;
    JSS_UNLOCK(mixer);
}


BOOL jvmGetMute(JSSMixer * mixer, const int channel)
{
    BOOL tmp;

    JSS_LOCK(mixer);
    tmp = mixer->channels[channel].chMute;
    JSS_UNLOCK(mixer);

    return tmp;
}


void jvmClear(JSSMixer * mixer, const int channel)
{
    JSS_LOCK(mixer);
    memset(&mixer->channels[channel], 0, sizeof(JSSChannel));
    JSS_UNLOCK(mixer);
}


void jvmSetGlobalVol(JSSMixer * mixer, const int volume)
{
    JSS_LOCK(mixer);
    mixer->globalVol = volume;
    JSS_UNLOCK(mixer);
}


int jvmGetGlobalVol(JSSMixer * mixer)
{
    int tmp;

    JSS_LOCK(mixer);
    tmp = mixer->globalVol;
    JSS_UNLOCK(mixer);

    return tmp;
}
