#include "SND_exChannel.h"

#include "SND_channel.h"
#include "SND_main.h"
#include "SND_util.h"
#include "SND_work.h"

#include "registers.h"
#include "code32.h"

// TODO import these tables into here if we have a working .rodata section
extern const u8 sChannelAllocationOrder[SND_CHANNEL_COUNT];
extern const u8 sAttackCoeffTable[19];
extern const u8 sSampleDataShiftTable[4];

static u32 sLockedChannelMask;
static u32 sWeakLockedChannelMask;

static u16 CalcDecayCoeff(int vol);

static int ExChannelSweepUpdate(struct SNDExChannel *chn, BOOL step);
static int ExChannelLfoUpdate(struct SNDExChannel *chn, BOOL step);
static void ExChannelStart(struct SNDExChannel *chn, int length);
static int ExChannelVolumeCmp(struct SNDExChannel *chn_a, struct SNDExChannel *chn_b);
static void ExChannelSetup(
    struct SNDExChannel *, SNDExChannelCallback callback, void *callbackUserData, int priority);

void SND_ExChannelInit(void)
{
    struct SNDExChannel *chn;
    s32 i;

    for (i = 0; i < SND_CHANNEL_COUNT; i++)
    {
        chn = &SNDi_Work.channels[i];

        chn->id = (u8)i;
        chn->flags.syncFlag = 0;
        chn->flags.active = FALSE;
    }

    sLockedChannelMask = 0;
    sWeakLockedChannelMask = 0;
}

void SND_UpdateExChannel(void)
{
    struct SNDExChannel *chn;
    s32 i;

    for (i = 0; i < SND_CHANNEL_COUNT; i++)
    {
        chn = &SNDi_Work.channels[i];

        if (chn->flags.syncFlag == 0)
            continue;

        if (chn->flags.syncFlag & SND_CHN_SYNC_STOP)
            SND_StopChannel(i, 0);

        if (chn->flags.syncFlag & SND_CHN_SYNC_START)
        {
            switch (chn->type)
            {
                case SND_CHN_TYPE_PCM:
                    SND_SetupChannelPcm(i,
                        chn->waveDataPtr,
                        chn->waveParam.format,
                        chn->waveParam.loopEnabled ? 1 : 2,
                        (s32)chn->waveParam.loopStart,
                        (s32)chn->waveParam.loopLength,
                        chn->volume & 0xFF,
                        chn->volume >> 8,
                        chn->timer,
                        chn->pan);
                    break;
                case SND_CHN_TYPE_PSG:
                    SND_SetupChannelPsg(i,
                        chn->dutyCycle,
                        chn->volume & 0xFF,
                        chn->volume >> 8,
                        chn->timer,
                        chn->pan);
                    break;
                case SND_CHN_TYPE_NOISE:
                    SND_SetupChannelNoise(
                        i, chn->volume & 0xFF, chn->volume >> 8, chn->timer, chn->pan);
                    break;
            }
        }
        else
        {
            if (chn->flags.syncFlag & SND_CHN_SYNC_TIMER)
            {
                SND_SetChannelTimer(i, chn->timer);
            }
            if (chn->flags.syncFlag & SND_CHN_SYNC_VOLUME)
            {
                SND_SetChannelVolume(i, chn->volume & 0xFF, chn->volume >> 8);
            }
            if (chn->flags.syncFlag & SND_CHN_SYNC_PAN)
            {
                SND_SetChannelPan(i, chn->pan);
            }
        }
    }

    for (i = 0; i < SND_CHANNEL_COUNT; i++)
    {
        chn = &SNDi_Work.channels[i];

        if (!chn->flags.syncFlag)
            continue;

        if (chn->flags.syncFlag & SND_CHN_SYNC_START)
            reg_SOUNDxCNT_STAT(i) |= 0x80;
        chn->flags.syncFlag = 0;
    }
}

void SND_ExChannelMain(BOOL step)
{
    struct SNDExChannel *chn;
    s32 i;
    s32 vol;
    s32 pitch;
    s32 pan;
    s32 lfo;
    u16 newTimer;

    for (i = 0; i < SND_CHANNEL_COUNT; i++)
    {
        vol = 0;
        pitch = 0;
        pan = 0;
        chn = &SNDi_Work.channels[i];

        if (!chn->flags.active)
            continue;

        if (chn->flags.start)
        {
            chn->flags.syncFlag |= SND_CHN_SYNC_START;
            chn->flags.start = FALSE;
        }
        else if (!SND_IsChannelActive(i))
        {
            if (chn->callback)
                chn->callback(chn, 1, chn->callbackUserData);
            else
                chn->priority = 0;
            chn->volume = 0;
            chn->flags.active = FALSE;
            continue;
        }

        vol += SNDi_DecibelSquareTable[chn->velocity];
        pitch += (chn->midiKey - chn->rootMidiKey) * 0x40;

        vol += SND_UpdateExChannelEnvelope(chn, step);
        pitch += ExChannelSweepUpdate(chn, step);

        vol += chn->userDecay;
        vol += chn->userDecay2;
        pitch += chn->userPitch;

        lfo = ExChannelLfoUpdate(chn, step);

        switch (chn->lfo.param.target)
        {
            case SND_LFO_VOLUME:
                if (vol > -0x8000)
                    vol += lfo;
                break;
            case SND_LFO_PAN:
                pan += lfo;
                break;
            case SND_LFO_PITCH:
                pitch += lfo;
                break;
        }

        pan += chn->initPan;
        if (chn->panRange != 127)
        {
            pan = (pan * chn->panRange + 0x40) >> 7;
        }
        pan += chn->userPan;

        if (chn->envStatus == SND_ENV_RELEASE && vol <= -723)
        {
            chn->flags.syncFlag = SND_CHN_SYNC_STOP;
            if (chn->callback)
                chn->callback(chn, 1, chn->callbackUserData);
            else
                chn->priority = 0;
            chn->volume = 0;
            chn->flags.active = 0;
        }
        else
        {
            vol = SND_CalcChannelVolume(vol);
            newTimer = SND_CalcTimer(chn->waveParam.timer, pitch);

            if (chn->type == SND_CHN_TYPE_PSG)
                newTimer &= 0xFFFC;

            pan += 0x40;
            if (pan < 0)
                pan = 0;
            else if (pan > 127)
                pan = 127;

            if (vol != chn->volume)
            {
                chn->volume = (u16)vol;
                chn->flags.syncFlag |= SND_CHN_SYNC_VOLUME;
            }
            if (newTimer != chn->timer)
            {
                chn->timer = (u16)newTimer;
                chn->flags.syncFlag |= SND_CHN_SYNC_TIMER;
            }
            if (pan != chn->pan)
            {
                chn->pan = (u8)pan;
                chn->flags.syncFlag |= SND_CHN_SYNC_PAN;
            }
        }
    }
}

BOOL SND_StartExChannelPcm(
    struct SNDExChannel *chn, const struct SNDWaveParam *wave, const void *data, s32 length)
{
    chn->type = SND_CHN_TYPE_PCM;
    chn->waveParam = *wave;
    chn->waveDataPtr = data;
    ExChannelStart(chn, length);
    return TRUE;
}

BOOL SND_StartExChannelPsg(struct SNDExChannel *chn, s32 duty, s32 length)
{
    if (chn->id < 8)
    {
        return FALSE;
    }
    else if (chn->id > 13)
    {
        return FALSE;
    }
    else
    {
        chn->type = SND_CHN_TYPE_PSG;
        chn->dutyCycle = duty;
        chn->waveParam.timer = 8006;
        ExChannelStart(chn, length);
        return TRUE;
    }
}

BOOL SND_StartExChannelNoise(struct SNDExChannel *chn, s32 length)
{
    if (chn->id < 14)
    {
        return FALSE;
    }
    else if (chn->id > 15)
    {
        return FALSE;
    }
    else
    {
        chn->type = SND_CHN_TYPE_NOISE;
        chn->waveParam.timer = 8006;
        ExChannelStart(chn, length);
        return TRUE;
    }
}

s32 SND_UpdateExChannelEnvelope(struct SNDExChannel *chn, BOOL step)
{
    s32 sustain;

    if (step)
    {
        switch (chn->envStatus)
        {
            case SND_ENV_ATTACK:
                chn->envAttenuation = -((-chn->envAttenuation * chn->envAttack) >> 8);
                if (chn->envAttenuation == 0)
                    chn->envStatus = SND_ENV_DECAY;
                break;
            case SND_ENV_DECAY:
                sustain = SNDi_DecibelSquareTable[chn->envSustain] << 7;
                chn->envAttenuation -= chn->envDecay;
                if (chn->envAttenuation <= sustain)
                {
                    chn->envAttenuation = sustain;
                    chn->envStatus = SND_ENV_SUSTAIN;
                }
                break;
            case SND_ENV_SUSTAIN:
                break;
            case SND_ENV_RELEASE:
                chn->envAttenuation -= chn->envRelease;
                break;
        }
    }

    return chn->envAttenuation >> 7;
}

void SND_SetExChannelAttack(struct SNDExChannel *chn, s32 attack)
{
    if (attack < 109)
        chn->envAttack = (u8)(255 - attack);
    else
        chn->envAttack = sAttackCoeffTable[127 - attack];
}

void SND_SetExChannelDecay(struct SNDExChannel *chn, s32 decay)
{
    chn->envDecay = CalcDecayCoeff(decay);
}

void SND_SetExChannelSustain(struct SNDExChannel *chn, s32 sustain)
{
    chn->envSustain = (u8)sustain;
}

void SND_SetExChannelRelease(struct SNDExChannel *chn, s32 release)
{
    chn->envRelease = CalcDecayCoeff(release);
}

void SND_ReleaseExChannel(struct SNDExChannel *chn)
{
    chn->envStatus = SND_ENV_RELEASE;
}

BOOL SND_IsExChannelActive(struct SNDExChannel *chn)
{
    return chn->flags.active;
}

struct SNDExChannel *SND_AllocExChannel(
    u32 channelMask, int priority, u32 flags, SNDExChannelCallback callback, void *callbackUserData)
{
    struct SNDExChannel *chnPrev;
    int i;
    struct SNDExChannel *chn;
    u8 channelCandidate;

    channelMask &= ~sLockedChannelMask;
    if (flags == 0)
        channelMask &= ~sWeakLockedChannelMask;

    chnPrev = NULL;

    for (i = 0; i < SND_CHANNEL_COUNT; i++)
    {
        channelCandidate = sChannelAllocationOrder[i];

        if (channelMask & (1 << channelCandidate))
        {
            chn = &SNDi_Work.channels[channelCandidate];

            if (chnPrev == NULL)
            {
                chnPrev = chn;
                continue;
            }

            if (chn->priority > chnPrev->priority)
                continue;

            if (chn->priority != chnPrev->priority || ExChannelVolumeCmp(chnPrev, chn) < 0)
                chnPrev = chn;
        }
    }

    if (chnPrev == NULL)
        return NULL;

    if (priority < chnPrev->priority)
        return NULL;

    if (chnPrev->callback)
        chnPrev->callback(chnPrev, 0, chnPrev->callbackUserData);

    chnPrev->flags.syncFlag = 2;
    chnPrev->flags.active = 0;
    ExChannelSetup(chnPrev, callback, callbackUserData, priority);
    return chnPrev;
}

void SND_FreeExChannel(struct SNDExChannel *chn)
{
    if (chn)
    {
        chn->callback = NULL;
        chn->callbackUserData = NULL;
    }
}

void SND_StopUnlockedChannel(u32 channelMask, u32 weak)
{
    (void)weak;

    struct SNDExChannel *chn;

    for (int i = 0; i < SND_CHANNEL_COUNT && channelMask != 0; i++, channelMask >>= 1)
    {
        if ((channelMask & 1) == 0)
            continue;

        chn = &SNDi_Work.channels[i];

        if (sLockedChannelMask & (1 << i))
            continue;

        if (chn->callback)
            chn->callback(chn, 0, chn->callbackUserData);

        SND_StopChannel(i, 0);
        chn->priority = 0;
        SND_FreeExChannel(chn);
        chn->flags.syncFlag = 0;
        chn->flags.active = 0;
    }
}

void SND_LockChannel(u32 channelMask, u32 weak)
{
    struct SNDExChannel *chn;
    u32 j = channelMask;
    int i = 0;

    for (; i < SND_CHANNEL_COUNT && j != 0; i++, j >>= 1)
    {
        if ((j & 1) == 0)
            continue;

        chn = &SNDi_Work.channels[i];

        if (sLockedChannelMask & (1 << i))
            continue;

        if (chn->callback)
            chn->callback(chn, 0, chn->callbackUserData);

        SND_StopChannel(i, 0);
        chn->priority = 0;
        SND_FreeExChannel(chn);
        chn->flags.syncFlag = 0;
        chn->flags.active = 0;
    }

    if (weak & 1)
    {
        sWeakLockedChannelMask |= channelMask;
    }
    else
    {
        sLockedChannelMask |= channelMask;
    }
}

void SND_UnlockChannel(u32 channelMask, u32 weak)
{
    if (weak & 1)
    {
        sWeakLockedChannelMask &= ~channelMask;
    }
    else
    {
        sLockedChannelMask &= ~channelMask;
    }
}

u32 SND_GetLockedChannel(u32 weak)
{
    if (weak & 1)
    {
        return sWeakLockedChannelMask;
    }
    else
    {
        return sLockedChannelMask;
    }
}

void SND_InvalidateWave(const void *start, const void *end)
{
    for (u8 i = 0; i < SND_CHANNEL_COUNT; i++)
    {
        struct SNDExChannel *chn = &SNDi_Work.channels[i];

        if (chn->flags.active && chn->type == 0 && start <= chn->waveDataPtr &&
            chn->waveDataPtr <= end)
        {
            chn->flags.start = FALSE;
            SND_StopChannel(i, 0);
        }
    }
}

void SND_InitLfoParam(struct SNDLfoParam *lfoParam)
{
    lfoParam->target = SND_LFO_PITCH;
    lfoParam->depth = 0;
    lfoParam->range = 1;
    lfoParam->speed = 16;
    lfoParam->delay = 0;
}

void SND_StartLfo(struct SNDLfo *lfo)
{
    lfo->counter = 0;
    lfo->delayCounter = 0;
}

void SND_UpdateLfo(struct SNDLfo *lfo)
{
    if (lfo->delayCounter < lfo->param.delay)
    {
        lfo->delayCounter++;
    }
    else
    {
        u32 tmp = lfo->counter;
        tmp += lfo->param.speed << 6;
        tmp >>= 8;
        while (tmp >= 0x80)
        {
            tmp -= 0x80;
        }
        lfo->counter += lfo->param.speed << 6;
        lfo->counter &= 0xFF;
        lfo->counter |= tmp << 8;
    }
}

int SND_GetLfoValue(struct SNDLfo *lfo)
{
    if (lfo->param.depth == 0)
    {
        return 0;
    }
    else if (lfo->delayCounter < lfo->param.delay)
    {
        return 0;
    }
    else
    {
        return SND_SinIdx((s32)((u32)lfo->counter >> 8)) * lfo->param.depth * lfo->param.range;
    }
}

static u16 CalcDecayCoeff(int vol)
{
    if (vol == 127)
        return 0xFFFF;
    else if (vol == 126)
        return 0x3C00;
    else if (vol < 50)
        return (u16)(vol * 2 + 1);
    else
        return (u16)(0x1E00 / (126 - vol));
}

static void ExChannelSetup(
    struct SNDExChannel *chn, SNDExChannelCallback callback, void *callbackUserData, int priority)
{
    chn->channelLLNext = NULL;
    chn->callback = callback;
    chn->callbackUserData = callbackUserData;
    chn->length = 0;
    chn->priority = (u8)priority;
    chn->volume = 127;
    chn->flags.start = FALSE;
    chn->flags.autoSweep = TRUE;
    chn->midiKey = 60;
    chn->rootMidiKey = 60;
    chn->velocity = 127;
    chn->initPan = 0;
    chn->userDecay = 0;
    chn->userDecay2 = 0;
    chn->userPitch = 0;
    chn->userPan = 0;
    chn->panRange = 127;
    chn->sweepPitch = 0;
    chn->sweepLength = 0;
    chn->sweepCounter = 0;

    SND_SetExChannelAttack(chn, 127);
    SND_SetExChannelDecay(chn, 127);
    SND_SetExChannelSustain(chn, 127);
    SND_SetExChannelRelease(chn, 127);
    SND_InitLfoParam(&chn->lfo.param);
}

static void ExChannelStart(struct SNDExChannel *chn, int length)
{
    chn->envAttenuation = -92544;
    chn->envStatus = 0;
    chn->length = length;
    SND_StartLfo(&chn->lfo);
    chn->flags.start = TRUE;
    chn->flags.active = TRUE;
}

static int ExChannelVolumeCmp(struct SNDExChannel *chn_a, struct SNDExChannel *chn_b)
{
    int vol_a = chn_a->volume & 0xFF;
    int vol_b = chn_b->volume & 0xFF;

    vol_a <<= 4;
    vol_b <<= 4;

    vol_a >>= sSampleDataShiftTable[chn_a->volume >> 8];
    vol_b >>= sSampleDataShiftTable[chn_b->volume >> 8];

    if (vol_a != vol_b)
    {
        if (vol_a < vol_b)
            return 1;
        else
            return -1;
    }
    return 0;
}

static int ExChannelSweepUpdate(struct SNDExChannel *chn, BOOL step)
{
    s64 result;

    if (chn->sweepPitch == 0)
    {
        result = 0;
    }
    else if (chn->sweepCounter >= chn->sweepLength)
    {
        result = 0;
    }
    else
    {
        result = (s64)chn->sweepPitch * (chn->sweepLength - chn->sweepCounter) / chn->sweepLength;

        if (step && chn->flags.autoSweep)
            chn->sweepCounter++;
    }

    return (int)result;
}

static int ExChannelLfoUpdate(struct SNDExChannel *chn, BOOL step)
{
    s64 result = SND_GetLfoValue(&chn->lfo);

    if (result != 0)
    {
        switch (chn->lfo.param.target)
        {
            case SND_LFO_VOLUME:
                result *= 60;
                break;
            case SND_LFO_PITCH:
                result <<= 6;
                break;
            case SND_LFO_PAN:
                result <<= 6;
                break;
        }
        result >>= 14;
    }

    if (step)
    {
        SND_UpdateLfo(&chn->lfo);
    }

    return (int)result;
}
