/*
 * Apple // emulator for *ix
 *
 * This software package is subject to the GNU General Public License
 * version 3 or later (your choice) as published by the Free Software
 * Foundation.
 *
 * Copyright 2013-2015 Aaron Culliney
 *
 */

// soundcore OpenAL backend -- streaming audio

#include "common.h"

#ifdef __APPLE__
#   import <OpenAL/al.h>
#   import <OpenAL/alc.h>
#else
#   include <AL/al.h>
#   include <AL/alc.h>
#   include <AL/alext.h>
#endif

#include "audio/alhelpers.h"
#include "playqueue.h"
#include "uthash.h"

#define DEBUG_OPENAL 0
#if DEBUG_OPENAL
#   define OPENAL_LOG(...) LOG(__VA_ARGS__)
#else
#   define OPENAL_LOG(...)
#endif

#define OPENAL_NUM_BUFFERS 4

typedef struct ALVoice {
    ALuint source;
    ALuint buffers[OPENAL_NUM_BUFFERS];

    // playing data
    PlayQueue_s *playq;
    ALint _queued_total_bytes; // a maximum estimate -- actual value depends on OpenAL query

    // working data buffer
    ALbyte *data;
    ALsizei index;      // working buffer byte index
    ALsizei buffersize; // working buffer size (and OpenAL buffersize)

    ALsizei replay_index;

    // sample parameters
    ALenum format;
    ALenum channels;
    ALenum type;
    ALuint rate;
} ALVoice;

typedef struct ALVoices {
    const ALuint source;
    ALVoice *voice;
    UT_hash_handle hh;
} ALVoices;

static ALVoices *voices = NULL;
static AudioBackend_s openal_audio_backend = { { 0 } };

// ----------------------------------------------------------------------------
// AudioBuffer_s processing routines

static void _playq_removeNode(ALVoice *voice, PlayNode_s *playNode) {
    long err = voice->playq->Remove(voice->playq, playNode);
    assert(err == 0);
    voice->_queued_total_bytes -= playNode->numBytes;
    assert(voice->_queued_total_bytes >= 0);
}

static long _ALProcessPlayBuffers(ALVoice *voice, ALuint *bytes_queued) {
    long err = 0;
    *bytes_queued = 0;

    do {
        ALint processed = 0;
        alGetSourcei(voice->source, AL_BUFFERS_PROCESSED, &processed);
        if ((err = alGetError()) != AL_NO_ERROR) {
            ERRLOG("OOPS, error in checking processed buffers : 0x%08lx", err);
            break;
        }

        while (processed > 0) {
            --processed;
            ALuint bufid = 0;
            alSourceUnqueueBuffers(voice->source, 1, &bufid);
            if ((err = alGetError()) != AL_NO_ERROR) {
                ERRLOG("OOPS, OpenAL error dequeuing buffer : 0x%08lx", err);
                break;
            }

            OPENAL_LOG("Attempting to dequeue %u ...", bufid);
            PlayNode_s playNode = {
                .nodeId = bufid,
            };
            err = voice->playq->Get(voice->playq, &playNode);
            if (err) {
                ERRLOG("OOPS, OpenAL bufid %u not found in playlist...", bufid);
            } else {
                _playq_removeNode(voice, &playNode);
            }
        }

        ALint play_offset = 0;
        alGetSourcei(voice->source, AL_BYTE_OFFSET, &play_offset);
        if ((err = alGetError()) != AL_NO_ERROR) {
            ERRLOG("OOPS, alGetSourcei AL_BYTE_OFFSET : 0x%08lx", err);
            break;
        }
        assert((play_offset >= 0)/* && (play_offset < voice->buffersize)*/);

        long q = voice->_queued_total_bytes/* + voice->index*/ - play_offset;

        if (q >= 0) {
            *bytes_queued = (ALuint)q;
        }
    } while (0);

    return err;
}

// returns queued+working sound buffer size in bytes
static long ALGetPosition(AudioBuffer_s *_this, OUTPARM unsigned long *bytes_queued) {
    *bytes_queued = 0;
    long err = 0;

    do {
        ALVoice *voice = (ALVoice*)_this->_internal;

        ALuint queued = 0;
        long err = _ALProcessPlayBuffers(voice, &queued);
        if (err) {
            break;
        }
#if DEBUG_OPENAL
        static int last_queued = 0;
        if (queued != last_queued) {
            last_queued = queued;
            OPENAL_LOG("OpenAL bytes queued : %u", queued);
        }
#endif

        *bytes_queued = queued + voice->index;
    } while (0);

    return err;
}

static long ALLockBuffer(AudioBuffer_s *_this, unsigned long write_bytes, INOUT int16_t **audio_ptr, OUTPARM unsigned long *audio_bytes) {
    *audio_bytes = 0;
    *audio_ptr = NULL;
    long err = 0;

    do {
        ALVoice *voice = (ALVoice*)_this->_internal;

        if (write_bytes == 0) {
            write_bytes = voice->buffersize;
        }

        ALuint bytes_queued = 0;
        err = _ALProcessPlayBuffers(voice, &bytes_queued);
        if (err) {
            break;
        }

        if ((bytes_queued == 0) && (voice->index == 0)) {
            LOG("Buffer underrun ... queuing quiet samples ...");
            int quiet_size = voice->buffersize>>2/* 1/4 buffer */;
            memset(voice->data, 0x0, quiet_size);
            voice->index += quiet_size;
        }
#if 0
        else if (bytes_queued + voice->index < (voice->buffersize>>3)/* 1/8 buffer */)
        {
            OPENAL_LOG("Potential underrun ...");
        }
#endif

        ALsizei remaining = voice->buffersize - voice->index;
        if (write_bytes > remaining) {
            write_bytes = remaining;
        }

        *audio_ptr = (int16_t *)(voice->data+voice->index);
        *audio_bytes = write_bytes;
    } while (0);

    return err;
}

static long _ALSubmitBufferToOpenAL(ALVoice *voice) {
    long err = 0;

    do {
        // Micro-manage play queue locally to understand the total bytes-in-play
        PlayNode_s playNode = {
            .nodeId = INVALID_NODE_ID,
            .numBytes = voice->index,
            .bytes = (uint8_t *)(voice->data),
        };
        err = voice->playq->Enqueue(voice->playq, &playNode);
        if (err) {
            break;
        }
        voice->_queued_total_bytes += voice->index;
        voice->index = 0;
        assert(voice->_queued_total_bytes > 0);

        OPENAL_LOG("Enqueing OpenAL buffer %ld (%lu bytes) at %p", playNode.nodeId, playNode.numBytes, playNode.bytes);
        alBufferData(playNode.nodeId, voice->format, playNode.bytes, playNode.numBytes, voice->rate);
        if ((err = alGetError()) != AL_NO_ERROR) {
            _playq_removeNode(voice, &playNode);
            ERRLOG("OOPS, Error alBufferData : 0x%08lx", err);
            break;
        }

        ALuint nodeId = (ALuint)playNode.nodeId;
        alSourceQueueBuffers(voice->source, 1, &nodeId);
        if ((err = alGetError()) != AL_NO_ERROR) {
            _playq_removeNode(voice, &playNode);
            ERRLOG("OOPS, Error buffering data : 0x%08lx", err);
            break;
        }

        ALint state = 0;
        alGetSourcei(voice->source, AL_SOURCE_STATE, &state);
        if ((err = alGetError()) != AL_NO_ERROR) {
            ERRLOG("OOPS, Error checking source state : 0x%08lx", err);
            break;
        }
        if ((state != AL_PLAYING) && (state != AL_PAUSED)) {
            // 2013/11/17 NOTE : alSourcePlay() is expensive and causes audio artifacts, only invoke if needed
            LOG("Restarting playback (was 0x%08x) ...", state);
            alSourcePlay(voice->source);
            if ((err = alGetError()) != AL_NO_ERROR) {
                LOG("Error starting playback : 0x%08lx", err);
                break;
            }
        }
    } while (0);

    return err;
}

static long ALUnlockBuffer(AudioBuffer_s *_this, unsigned long audio_bytes) {
    long err = 0;

    do {
        ALVoice *voice = (ALVoice*)_this->_internal;
        ALuint bytes_queued = 0;
        err = _ALProcessPlayBuffers(voice, &bytes_queued);
        if (err) {
            break;
        }

        voice->index += audio_bytes;


        if (voice->index >= voice->buffersize) {
            assert((voice->index == voice->buffersize) && "OOPS, detected an actual overflow in queued sound data");
        }

        if (bytes_queued >= (voice->buffersize>>2)/*quarter buffersize*/) {
            // keep accumulating data into working buffer
            break;
        }

        if (! (voice->playq->CanEnqueue(voice->playq)) ) {
            OPENAL_LOG("no free audio buffers"); // keep accumulating ...
            break;
        }

        // Submit working buffer to OpenAL

        err = _ALSubmitBufferToOpenAL(voice);
    } while (0);

    return err;
}

#if 0
// HACK Part I : done once for mockingboard that has semiauto repeating phonemes ...
static long ALUnlockStaticBuffer(AudioBuffer_s *_this, unsigned long audio_bytes) {
    ALVoice *voice = (ALVoice*)_this->_internal;
    voice->replay_index = (ALsizei)audio_bytes;
    return 0;
}

// HACK Part II : replay mockingboard phoneme ...
static long ALReplay(AudioBuffer_s *_this) {
    ALVoice *voice = (ALVoice*)_this->_internal;
    voice->index = voice->replay_index;
    long err = _ALSubmitBufferToOpenAL(voice);
    return err;
}
#endif

static long ALGetStatus(AudioBuffer_s *_this, OUTPARM unsigned long *status) {
    *status = -1;
    long err = 0;

    do {
        ALVoice* voice = (ALVoice*)_this->_internal;
        ALint state = 0;
        alGetSourcei(voice->source, AL_SOURCE_STATE, &state);
        if ((err = alGetError()) != AL_NO_ERROR) {
            ERRLOG("OOPS, Error checking source state : 0x%08lx", err);
            break;
        }

        if ((state == AL_PLAYING) || (state == AL_PAUSED)) {
            *status = AUDIO_STATUS_PLAYING;
        } else {
            *status = AUDIO_STATUS_NOTPLAYING;
        }
    } while (0);

    return err;
}

// ----------------------------------------------------------------------------
// ALVoice is the AudioBuffer_s->_internal

static void _openal_destroyVoice(ALVoice *voice) {
    alDeleteSources(1, &voice->source);
    if (alGetError() != AL_NO_ERROR) {
        ERRLOG("OOPS, Failed to delete source");
    }

    if (voice->data) {
        FREE(voice->data);
    }

    for (unsigned int i=0; i<OPENAL_NUM_BUFFERS; i++) {
        alDeleteBuffers(1, voice->buffers);
        if (alGetError() != AL_NO_ERROR) {
            ERRLOG("OOPS, Failed to delete object IDs");
        }
    }

    playq_destroyPlayQueue(&(voice->playq));

    memset(voice, 0, sizeof(*voice));
    FREE(voice);
}

static ALVoice *_openal_createVoice(unsigned long numChannels) {
    ALVoice *voice = NULL;

    do {
        voice = calloc(1, sizeof(*voice));
        if (voice == NULL) {
            ERRLOG("OOPS, Out of memory!");
            break;
        }

        alGenBuffers(OPENAL_NUM_BUFFERS, voice->buffers);
        if (alGetError() != AL_NO_ERROR) {
            ERRLOG("OOPS, Could not create buffers");
            break;
        }

        alGenSources(1, &voice->source);
        if (alGetError() != AL_NO_ERROR) {
            ERRLOG("OOPS, Could not create source");
            break;
        }

        // Set parameters so mono sources play out the front-center speaker and won't distance attenuate.
        alSource3i(voice->source, AL_POSITION, 0, 0, -1);
        if (alGetError() != AL_NO_ERROR) {
            ERRLOG("OOPS, Could not set AL_POSITION source parameter");
            break;
        }
        alSourcei(voice->source, AL_SOURCE_RELATIVE, AL_TRUE);
        if (alGetError() != AL_NO_ERROR) {
            ERRLOG("OOPS, Could not set AL_SOURCE_RELATIVE source parameter");
            break;
        }
        alSourcei(voice->source, AL_ROLLOFF_FACTOR, 0);
        if (alGetError() != AL_NO_ERROR) {
            ERRLOG("OOPS, Could not set AL_ROLLOFF_FACTOR source parameter");
            break;
        }

#if 0
        alSourcei(voice->source, AL_STREAMING, AL_TRUE);
        if (alGetError() != AL_NO_ERROR) {
            ERRLOG("OOPS, Could not set AL_STREAMING source parameter");
            break;
        }
#endif

        long longBuffers[OPENAL_NUM_BUFFERS];
        for (unsigned int i=0; i<OPENAL_NUM_BUFFERS; i++) {
            longBuffers[i] = (long)(voice->buffers[i]);
        }
        voice->playq = playq_createPlayQueue(longBuffers, OPENAL_NUM_BUFFERS);
        if (!voice->playq) {
            ERRLOG("OOPS, Not enough memory for PlayQueue");
            break;
        }

        voice->rate = openal_audio_backend.systemSettings.sampleRateHz;

        // Emulator supports only mono and stereo
        if (numChannels == 2) {
            voice->format = AL_FORMAT_STEREO16;
        } else {
            voice->format = AL_FORMAT_MONO16;
        }

        /* Allocate enough space for the temp buffer, given the format */
        assert(numChannels == 1 || numChannels == 2);
        unsigned long maxSamples = openal_audio_backend.systemSettings.monoBufferSizeSamples * numChannels;
        voice->buffersize = maxSamples * openal_audio_backend.systemSettings.bytesPerSample;

        voice->data = calloc(1, voice->buffersize);
        if (voice->data == NULL) {
            ERRLOG("OOPS, Error allocating %d bytes", voice->buffersize);
            break;
        }

        LOG("\tRate     : 0x%08x", voice->rate);
        LOG("\tFormat   : 0x%08x", voice->format);
        LOG("\tbuffersize : %d", voice->buffersize);

        return voice;

    } while(0);

    // ERR
    if (voice) {
        _openal_destroyVoice(voice);
    }

    return NULL;
}

// ----------------------------------------------------------------------------

static long openal_destroySoundBuffer(const struct AudioContext_s *sound_system, INOUT AudioBuffer_s **soundbuf_struct) {
    if (!*soundbuf_struct) {
        // already dealloced
        return 0;
    }

    LOG("openal_destroySoundBuffer ...");
    ALVoice *voice = (ALVoice *)((*soundbuf_struct)->_internal);
    ALint source = voice->source;

    _openal_destroyVoice(voice);

    ALVoices *vnode = NULL;
    HASH_FIND_INT(voices, &source, vnode);
    if (vnode) {
        HASH_DEL(voices, vnode);
        FREE(vnode);
    }

    FREE(*soundbuf_struct);
    return 0;
}

static long openal_createSoundBuffer(const AudioContext_s *audio_context, INOUT AudioBuffer_s **soundbuf_struct) {
    LOG("openal_createSoundBuffer ...");
    assert(*soundbuf_struct == NULL);

    ALVoice *voice = NULL;

    do {

        ALCcontext *ctx = (ALCcontext*)(audio_context->_internal);
        assert(ctx != NULL);

        if ((voice = _openal_createVoice(NUM_CHANNELS)) == NULL) {
            ERRLOG("OOPS, Cannot create new voice");
            break;
        }

        ALVoices immutableNode = { /*const*/.source = voice->source };
        ALVoices *vnode = calloc(1, sizeof(ALVoices));
        if (!vnode) {
            ERRLOG("OOPS, Not enough memory");
            break;
        }
        memcpy(vnode, &immutableNode, sizeof(ALVoices));
        vnode->voice = voice;
        HASH_ADD_INT(voices, source, vnode);

        if ((*soundbuf_struct = calloc(1, sizeof(AudioBuffer_s))) == NULL) {
            ERRLOG("OOPS, Not enough memory");
            break;
        }

        (*soundbuf_struct)->_internal          = voice;
        (*soundbuf_struct)->GetCurrentPosition = &ALGetPosition;
        (*soundbuf_struct)->Lock               = &ALLockBuffer;
        (*soundbuf_struct)->Unlock             = &ALUnlockBuffer;
        (*soundbuf_struct)->GetStatus          = &ALGetStatus;
        // mockingboard-specific hacks
        //(*soundbuf_struct)->UnlockStaticBuffer = &ALUnlockStaticBuffer;
        //(*soundbuf_struct)->Replay             = &ALReplay;

        return 0;
    } while(0);

    if (*soundbuf_struct) {
        openal_destroySoundBuffer(audio_context, soundbuf_struct);
    } else if (voice) {
        _openal_destroyVoice(voice);
    }

    return -1;
}

// ----------------------------------------------------------------------------

static long openal_systemShutdown(INOUT AudioContext_s **audio_context) {
    assert(*audio_context != NULL);

    ALCcontext *ctx = (ALCcontext*) (*audio_context)->_internal;
    assert(ctx != NULL);
    (*audio_context)->_internal = NULL;
    FREE(*audio_context);

    // NOTE : currently assuming just one OpenAL global context
    CloseAL();

    return 0;
}

static long openal_systemSetup(INOUT AudioContext_s **audio_context) {
    assert(*audio_context == NULL);
    assert(voices == NULL);

    long result = -1;
    ALCcontext *ctx = NULL;

    // 2015/06/29 these values seem to work well on Linux desktop ... no other OpenAL platform has been tested
    openal_audio_backend.systemSettings.sampleRateHz = 22050;
    openal_audio_backend.systemSettings.bytesPerSample = 2;
    openal_audio_backend.systemSettings.monoBufferSizeSamples = (8*1024);
    openal_audio_backend.systemSettings.stereoBufferSizeSamples = openal_audio_backend.systemSettings.monoBufferSizeSamples;

    do {

        if ((ctx = InitAL()) == NULL) {
            // NOTE : currently assuming just one OpenAL global context
            ERRLOG("OOPS, OpenAL initialize failed");
            break;
        }

        if (alIsExtensionPresent("AL_SOFT_buffer_samples")) {
            LOG("AL_SOFT_buffer_samples supported, good!");
        } else {
            LOG("WARNING - AL_SOFT_buffer_samples extension not supported... Proceeding anyway...");
        }

        if ((*audio_context = calloc(1, sizeof(AudioContext_s))) == NULL) {
            ERRLOG("OOPS, Not enough memory");
            break;
        }

        (*audio_context)->_internal = ctx;
        (*audio_context)->CreateSoundBuffer = &openal_createSoundBuffer;
        (*audio_context)->DestroySoundBuffer = &openal_destroySoundBuffer;

        result = 0;
    } while(0);

    if (result) {
        if (ctx) {
            AudioContext_s *ctxPtr = calloc(1, sizeof(AudioContext_s));
            ctxPtr->_internal = ctx;
            openal_systemShutdown(&ctxPtr);
        }
        assert (*audio_context == NULL);
        LOG("OpenAL sound output disabled");
    }

    return result;
}

static long openal_systemPause(AudioContext_s *audio_context) {
    ALVoices *vnode = NULL;
    ALVoices *tmp = NULL;
    long err = 0;

    HASH_ITER(hh, voices, vnode, tmp) {
        alSourcePause(vnode->source);
        err = alGetError();
        if (err != AL_NO_ERROR) {
            ERRLOG("OOPS, Failed to pause source : 0x%08lx", err);
        }
    }

    return 0;
}

static long openal_systemResume(AudioContext_s *audio_context) {
    ALVoices *vnode = NULL;
    ALVoices *tmp = NULL;
    long err = 0;

    HASH_ITER(hh, voices, vnode, tmp) {
        alSourcePlay(vnode->source);
        err = alGetError();
        if (err != AL_NO_ERROR) {
            ERRLOG("OOPS, Failed to pause source : 0x%08lx", err);
        }
    }

    return 0;
}

__attribute__((constructor(CTOR_PRIORITY_EARLY)))
static void _init_openal(void) {
    LOG("Initializing OpenAL sound system");

    assert((audio_backend == NULL) && "there can only be one!");

    openal_audio_backend.setup            = &openal_systemSetup;
    openal_audio_backend.shutdown         = &openal_systemShutdown;
    openal_audio_backend.pause            = &openal_systemPause;
    openal_audio_backend.resume           = &openal_systemResume;

    audio_backend = &openal_audio_backend;
}

