/*
 * 86Box    A hypervisor and IBM PC system emulator that specializes in
 *          running old operating systems and software designed for IBM
 *          PC systems and compatibles from 1981 through fairly recent
 *          system designs based on the PCI bus.
 *
 *          This file is part of the 86Box distribution.
 *
 *          Interface to the OpenAL sound processing library.
 *
 * Authors: Sarah Walker, <https://pcem-emulator.co.uk/>
 *          Miran Grca, <mgrca8@gmail.com>
 *
 *          Copyright 2008-2019 Sarah Walker.
 *          Copyright 2016-2019 Miran Grca.
 */
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#undef AL_API
#undef ALC_API
#define AL_LIBTYPE_STATIC
#define ALC_LIBTYPE_STATIC

#include "AL/al.h"
#include "AL/alc.h"
#include "AL/alext.h"
#include <86box/86box.h>
#include <86box/midi.h>
#include <86box/sound.h>
#include <86box/plat_unused.h>


#define I_NORMAL 0
#define I_MUSIC  1
#define I_WT     2
#define I_CD     3
#define I_FDD    4
#define I_HDD    5
#define I_YM2151 6
#define I_CQM    7
#define I_MIDI   8

ALuint        buffers[4];        /* front and back buffers */
ALuint        buffers_music[4];  /* front and back buffers */
ALuint        buffers_wt[4];     /* front and back buffers */
ALuint        buffers_cd[4];     /* front and back buffers */
ALuint        buffers_fdd[4];    /* front and back buffers */
ALuint        buffers_hdd[4];    /* front and back buffers */
ALuint        buffers_ym2151[4]; /* front and back buffers */
ALuint        buffers_cqm[4];    /* front and back buffers */
ALuint        buffers_midi[4];   /* front and back buffers */
static ALuint source[9];         /* audio sources */

static int         initialized   = 0;
static int         sources       = 2;
static ALCcontext *Context;
static ALCdevice  *Device;

void
al_set_midi(const int freq, const int buf_size)
{
    midi_freq     = freq;
    midi_buf_size = buf_size;
}

const char *
sound_get_output_devices(void)
{
    if (alcIsExtensionPresent(NULL, "ALC_ENUMERATE_ALL_EXT"))
        return alcGetString(NULL, ALC_ALL_DEVICES_SPECIFIER);
    if (alcIsExtensionPresent(NULL, "ALC_ENUMERATION_EXT"))
        return alcGetString(NULL, ALC_DEVICE_SPECIFIER);
    return NULL;
}

int
sound_get_device_sample_rate(const char *device_name)
{
    const ALCchar *dev_name = (device_name && device_name[0]) ? device_name : NULL;
    ALCdevice     *dev      = alcOpenDevice(dev_name);
    ALCint         freq     = 0;

    if (dev != NULL) {
        ALCcontext *prev_ctx = alcGetCurrentContext();
        ALCcontext *ctx      = alcCreateContext(dev, NULL);
        if (ctx != NULL) {
            alcMakeContextCurrent(ctx);
            alcGetIntegerv(dev, ALC_FREQUENCY, 1, &freq);
            alcMakeContextCurrent(prev_ctx);
            alcDestroyContext(ctx);
        }
        alcCloseDevice(dev);
    }
    return (int) freq;
}

int
sound_get_device_supported_rates(const char *device_name, int *rates_out, int max_rates)
{
    /* Candidate rates: only those where rate/50 <= SOUNDBUFLEN to avoid overflowing
       static device buffers. */
    static const int candidates[] = { FREQ_44100, FREQ_48000 };
    const int        num_cands    = (int) (sizeof(candidates) / sizeof(candidates[0]));

    const ALCchar *dev_name = (device_name && device_name[0]) ? device_name : NULL;
    ALCdevice     *dev      = alcOpenDevice(dev_name);
    int            count    = 0;

    if (dev != NULL) {
        ALCcontext *prev_ctx = alcGetCurrentContext();

        for (int i = 0; i < num_cands && count < max_rates; i++) {
            ALCint      attrs[] = { ALC_FREQUENCY, candidates[i], 0 };
            ALCcontext *ctx     = alcCreateContext(dev, attrs);
            ALCint      got     = 0;

            if (ctx != NULL) {
                alcMakeContextCurrent(ctx);
                alcGetIntegerv(dev, ALC_FREQUENCY, 1, &got);
                alcMakeContextCurrent(prev_ctx);
                alcDestroyContext(ctx);
            }

            if (got == candidates[i])
                rates_out[count++] = candidates[i];
        }

        alcCloseDevice(dev);
    }

    /* Fallback: if detection failed entirely, return all candidates. */
    if (count == 0) {
        for (int i = 0; i < num_cands && i < max_rates; i++)
            rates_out[i] = candidates[i];
        count = num_cands;
    }

    return count;
}

ALvoid
alutInit(UNUSED(ALint *argc), UNUSED(ALbyte **argv))
{
    /* Open device: use the user-selected device, or NULL for system default */
    const ALCchar *dev_name = (sound_output_device[0] != '\0') ? sound_output_device : NULL;
    Device = alcOpenDevice(dev_name);
    if (Device != NULL) {
        /* Create context(s) */
        Context = alcCreateContext(Device, NULL);
        if (Context != NULL) {
            /* Set active context */
            alcMakeContextCurrent(Context);
        }
    }
}

ALvoid
alutExit(ALvoid)
{
    if (Context != NULL) {
        /* Disable context */
        alcMakeContextCurrent(NULL);

        /* Release context(s) */
        alcDestroyContext(Context);

        if (Device != NULL) {
            /* Close device */
            alcCloseDevice(Device);
        }
    }
}

void
closeal(void)
{
    if (!initialized)
        return;

    alSourceStopv(sources, source);
    alDeleteSources(sources, source);

    if (sources >= 8)
        alDeleteBuffers(4, buffers_midi);
    alDeleteBuffers(4, buffers_cqm);
    alDeleteBuffers(4, buffers_fdd);
    alDeleteBuffers(4, buffers_hdd);
    alDeleteBuffers(4, buffers_cd);
    alDeleteBuffers(4, buffers_music);
    alDeleteBuffers(4, buffers);

    alutExit();

    initialized = 0;
}

void
inital(void)
{
    float   *buf              = NULL;
    float   *music_buf        = NULL;
    float   *wt_buf           = NULL;
    float   *cd_buf           = NULL;
    float   *midi_buf         = NULL;
    float   *fdd_buf          = NULL;
    float   *hdd_buf          = NULL;
    float   *ym2151_buf       = NULL;
    float   *cqm_buf          = NULL;
    int16_t *buf_int16        = NULL;
    int16_t *music_buf_int16  = NULL;
    int16_t *wt_buf_int16     = NULL;
    int16_t *cd_buf_int16     = NULL;
    int16_t *midi_buf_int16   = NULL;
    int16_t *ym2151_buf_int16 = NULL;
    int16_t *cqm_buf_int16    = NULL;
    int16_t *fdd_buf_int16    = NULL;
    int16_t *hdd_buf_int16    = NULL;

    int init_midi = 0;

    if (initialized)
        return;

    alutInit(0, 0);
    atexit(closeal);

    const char *mdn = midi_out_device_get_internal_name(midi_output_device_current);
    if ((strcmp(mdn, "none") != 0) && (strcmp(mdn, SYSTEM_MIDI_INTERNAL_NAME) != 0))
        init_midi = 1; /* If the device is neither none, nor system MIDI, initialize the
                          MIDI buffer and source, otherwise, do not. */

    const int pcm_buf_len = sound_sample_rate / 50;

    sources = 6 + !!init_midi;
    if (sound_is_float) {
        buf        = (float *) calloc((pcm_buf_len << 1), sizeof(float));
        music_buf  = (float *) calloc((MUSICBUFLEN << 1), sizeof(float));
        wt_buf     = (float *) calloc((WTBUFLEN << 1), sizeof(float));
        cd_buf     = (float *) calloc((CD_BUFLEN << 1), sizeof(float));
        fdd_buf    = (float *) calloc((pcm_buf_len << 1), sizeof(float));
        hdd_buf    = (float *) calloc((pcm_buf_len << 1), sizeof(float));
        ym2151_buf = (float *) calloc((YM2151BUFLEN << 1), sizeof(float));
        cqm_buf    = (float *) calloc((CQMBUFLEN << 1), sizeof(float));
        if (init_midi)
            midi_buf = (float *) calloc(midi_buf_size, sizeof(float));
    } else {
        buf_int16        = (int16_t *) calloc((pcm_buf_len << 1), sizeof(int16_t));
        music_buf_int16  = (int16_t *) calloc((MUSICBUFLEN << 1), sizeof(int16_t));
        wt_buf_int16     = (int16_t *) calloc((WTBUFLEN << 1), sizeof(int16_t));
        cd_buf_int16     = (int16_t *) calloc((CD_BUFLEN << 1), sizeof(int16_t));
        fdd_buf_int16    = (int16_t *) calloc((pcm_buf_len << 1), sizeof(int16_t));
        hdd_buf_int16    = (int16_t *) calloc((pcm_buf_len << 1), sizeof(int16_t));
        ym2151_buf_int16 = (int16_t *) calloc((YM2151BUFLEN << 1), sizeof(int16_t));
        cqm_buf_int16    = (int16_t *) calloc((CQMBUFLEN << 1), sizeof(int16_t));
        if (init_midi)
            midi_buf_int16 = (int16_t *) calloc(midi_buf_size, sizeof(int16_t));
    }

    alGenBuffers(4, buffers);
    alGenBuffers(4, buffers_cd);
    alGenBuffers(4, buffers_fdd);
    alGenBuffers(4, buffers_hdd);
    alGenBuffers(4, buffers_music);
    alGenBuffers(4, buffers_wt);
    alGenBuffers(4, buffers_ym2151);
    alGenBuffers(4, buffers_cqm);
    if (init_midi)
        alGenBuffers(4, buffers_midi);

    // Create sources: 0=main, 1=music, 2=wt, 3=cd, 4=fdd, 5=hdd, 6=midi(optional)
    if (init_midi)
        alGenSources(9, source);
    else
        alGenSources(8, source);

    alSource3f(source[I_NORMAL], AL_POSITION, 0.0f, 0.0f, 0.0f);
    alSource3f(source[I_NORMAL], AL_VELOCITY, 0.0f, 0.0f, 0.0f);
    alSource3f(source[I_NORMAL], AL_DIRECTION, 0.0f, 0.0f, 0.0f);
    alSourcef(source[I_NORMAL], AL_ROLLOFF_FACTOR, 0.0f);
    alSourcei(source[I_NORMAL], AL_SOURCE_RELATIVE, AL_TRUE);

    alSource3f(source[I_MUSIC], AL_POSITION, 0.0f, 0.0f, 0.0f);
    alSource3f(source[I_MUSIC], AL_VELOCITY, 0.0f, 0.0f, 0.0f);
    alSource3f(source[I_MUSIC], AL_DIRECTION, 0.0f, 0.0f, 0.0f);
    alSourcef(source[I_MUSIC], AL_ROLLOFF_FACTOR, 0.0f);
    alSourcei(source[I_MUSIC], AL_SOURCE_RELATIVE, AL_TRUE);

    alSource3f(source[I_WT], AL_POSITION, 0.0f, 0.0f, 0.0f);
    alSource3f(source[I_WT], AL_VELOCITY, 0.0f, 0.0f, 0.0f);
    alSource3f(source[I_WT], AL_DIRECTION, 0.0f, 0.0f, 0.0f);
    alSourcef(source[I_WT], AL_ROLLOFF_FACTOR, 0.0f);
    alSourcei(source[I_WT], AL_SOURCE_RELATIVE, AL_TRUE);

    alSource3f(source[I_CD], AL_POSITION, 0.0f, 0.0f, 0.0f);
    alSource3f(source[I_CD], AL_VELOCITY, 0.0f, 0.0f, 0.0f);
    alSource3f(source[I_CD], AL_DIRECTION, 0.0f, 0.0f, 0.0f);
    alSourcef(source[I_CD], AL_ROLLOFF_FACTOR, 0.0f);
    alSourcei(source[I_CD], AL_SOURCE_RELATIVE, AL_TRUE);

    alSource3f(source[I_FDD], AL_POSITION, 0.0f, 0.0f, 0.0f);
    alSource3f(source[I_FDD], AL_VELOCITY, 0.0f, 0.0f, 0.0f);
    alSource3f(source[I_FDD], AL_DIRECTION, 0.0f, 0.0f, 0.0f);
    alSourcef(source[I_FDD], AL_ROLLOFF_FACTOR, 0.0f);
    alSourcei(source[I_FDD], AL_SOURCE_RELATIVE, AL_TRUE);

    alSource3f(source[I_HDD], AL_POSITION, 0.0f, 0.0f, 0.0f);
    alSource3f(source[I_HDD], AL_VELOCITY, 0.0f, 0.0f, 0.0f);
    alSource3f(source[I_HDD], AL_DIRECTION, 0.0f, 0.0f, 0.0f);
    alSourcef(source[I_HDD], AL_ROLLOFF_FACTOR, 0.0f);
    alSourcei(source[I_HDD], AL_SOURCE_RELATIVE, AL_TRUE);

    alSource3f(source[I_YM2151], AL_POSITION, 0.0f, 0.0f, 0.0f);
    alSource3f(source[I_YM2151], AL_VELOCITY, 0.0f, 0.0f, 0.0f);
    alSource3f(source[I_YM2151], AL_DIRECTION, 0.0f, 0.0f, 0.0f);
    alSourcef(source[I_YM2151], AL_ROLLOFF_FACTOR, 0.0f);
    alSourcei(source[I_YM2151], AL_SOURCE_RELATIVE, AL_TRUE);

    alSource3f(source[I_CQM], AL_POSITION, 0.0f, 0.0f, 0.0f);
    alSource3f(source[I_CQM], AL_VELOCITY, 0.0f, 0.0f, 0.0f);
    alSource3f(source[I_CQM], AL_DIRECTION, 0.0f, 0.0f, 0.0f);
    alSourcef(source[I_CQM], AL_ROLLOFF_FACTOR, 0.0f);
    alSourcei(source[I_CQM], AL_SOURCE_RELATIVE, AL_TRUE);

    if (init_midi) {
        alSource3f(source[I_MIDI], AL_POSITION, 0.0f, 0.0f, 0.0f);
        alSource3f(source[I_MIDI], AL_VELOCITY, 0.0f, 0.0f, 0.0f);
        alSource3f(source[I_MIDI], AL_DIRECTION, 0.0f, 0.0f, 0.0f);
        alSourcef(source[I_MIDI], AL_ROLLOFF_FACTOR, 0.0f);
        alSourcei(source[I_MIDI], AL_SOURCE_RELATIVE, AL_TRUE);
    }

    if (sound_is_float) {
        memset(buf, 0, pcm_buf_len * 2 * sizeof(float));
        memset(cd_buf, 0, CD_BUFLEN * 2 * sizeof(float));
        memset(music_buf, 0, MUSICBUFLEN * 2 * sizeof(float));
        memset(wt_buf, 0, WTBUFLEN * 2 * sizeof(float));
        memset(fdd_buf, 0, pcm_buf_len * 2 * sizeof(float));
        memset(hdd_buf, 0, pcm_buf_len * 2 * sizeof(float));
        memset(ym2151_buf, 0, YM2151BUFLEN * 2 * sizeof(float));
        memset(cqm_buf, 0, CQMBUFLEN * 2 * sizeof(float));
        if (init_midi)
            memset(midi_buf, 0, midi_buf_size * sizeof(float));
    } else {
        memset(buf_int16, 0, pcm_buf_len * 2 * sizeof(int16_t));
        memset(cd_buf_int16, 0, CD_BUFLEN * 2 * sizeof(int16_t));
        memset(music_buf_int16, 0, MUSICBUFLEN * 2 * sizeof(int16_t));
        memset(wt_buf_int16, 0, WTBUFLEN * 2 * sizeof(int16_t));
        memset(fdd_buf_int16, 0, pcm_buf_len * 2 * sizeof(int16_t));
        memset(hdd_buf_int16, 0, pcm_buf_len * 2 * sizeof(int16_t));
        memset(ym2151_buf_int16, 0, YM2151BUFLEN * 2 * sizeof(int16_t));
        memset(cqm_buf_int16, 0, CQMBUFLEN * 2 * sizeof(int16_t));
        if (init_midi)
            memset(midi_buf_int16, 0, midi_buf_size * sizeof(int16_t));
    }

    for (uint8_t c = 0; c < 4; c++) {
        if (sound_is_float) {
            alBufferData(buffers[c], AL_FORMAT_STEREO_FLOAT32, buf, pcm_buf_len * 2 * (int) sizeof(float), sound_sample_rate);
            alBufferData(buffers_music[c], AL_FORMAT_STEREO_FLOAT32, music_buf, MUSICBUFLEN * 2 * (int) sizeof(float), MUSIC_FREQ);
            alBufferData(buffers_wt[c], AL_FORMAT_STEREO_FLOAT32, wt_buf, WTBUFLEN * 2 * (int) sizeof(float), WT_FREQ);
            alBufferData(buffers_cd[c], AL_FORMAT_STEREO_FLOAT32, cd_buf, CD_BUFLEN * 2 * (int) sizeof(float), CD_FREQ);
            alBufferData(buffers_fdd[c], AL_FORMAT_STEREO_FLOAT32, fdd_buf, pcm_buf_len * 2 * (int) sizeof(float), sound_sample_rate);
            alBufferData(buffers_hdd[c], AL_FORMAT_STEREO_FLOAT32, hdd_buf, pcm_buf_len * 2 * (int) sizeof(float), sound_sample_rate);
            alBufferData(buffers_ym2151[c], AL_FORMAT_STEREO_FLOAT32, ym2151_buf, YM2151BUFLEN * 2 * (int) sizeof(float), YM2151_FREQ);
            alBufferData(buffers_cqm[c], AL_FORMAT_STEREO_FLOAT32, cqm_buf, CQMBUFLEN * 2 * (int) sizeof(float), CQM_FREQ);
            if (init_midi)
                alBufferData(buffers_midi[c], AL_FORMAT_STEREO_FLOAT32, midi_buf, midi_buf_size * (int) sizeof(float), midi_freq);
        } else {
            alBufferData(buffers[c], AL_FORMAT_STEREO16, buf_int16, pcm_buf_len * 2 * (int) sizeof(int16_t), sound_sample_rate);
            alBufferData(buffers_music[c], AL_FORMAT_STEREO16, music_buf_int16, MUSICBUFLEN * 2 * (int) sizeof(int16_t), MUSIC_FREQ);
            alBufferData(buffers_wt[c], AL_FORMAT_STEREO16, wt_buf_int16, WTBUFLEN * 2 * (int) sizeof(int16_t), WT_FREQ);
            alBufferData(buffers_cd[c], AL_FORMAT_STEREO16, cd_buf_int16, CD_BUFLEN * 2 * (int) sizeof(int16_t), CD_FREQ);
            alBufferData(buffers_fdd[c], AL_FORMAT_STEREO16, fdd_buf_int16, pcm_buf_len * 2 * (int) sizeof(int16_t), sound_sample_rate);
            alBufferData(buffers_hdd[c], AL_FORMAT_STEREO16, hdd_buf_int16, pcm_buf_len * 2 * (int) sizeof(int16_t), sound_sample_rate);
            alBufferData(buffers_ym2151[c], AL_FORMAT_STEREO16, ym2151_buf_int16, YM2151BUFLEN * 2 * (int) sizeof(int16_t), YM2151_FREQ);
            alBufferData(buffers_cqm[c], AL_FORMAT_STEREO16, cqm_buf_int16, CQMBUFLEN * 2 * (int) sizeof(int16_t), CQM_FREQ);
            if (init_midi)
                alBufferData(buffers_midi[c], AL_FORMAT_STEREO16, midi_buf_int16, midi_buf_size * (int) sizeof(int16_t), midi_freq);
        }
    }

    alSourceQueueBuffers(source[I_NORMAL], 4, buffers);
    alSourceQueueBuffers(source[I_MUSIC], 4, buffers_music);
    alSourceQueueBuffers(source[I_WT], 4, buffers_wt);
    alSourceQueueBuffers(source[I_CD], 4, buffers_cd);
    alSourceQueueBuffers(source[I_FDD], 4, buffers_fdd);
    alSourceQueueBuffers(source[I_HDD], 4, buffers_hdd);
    alSourceQueueBuffers(source[I_YM2151], 4, buffers_ym2151);
    alSourceQueueBuffers(source[I_CQM], 4, buffers_cqm);
    if (init_midi)
        alSourceQueueBuffers(source[I_MIDI], 4, buffers_midi);
    alSourcePlay(source[I_NORMAL]);
    alSourcePlay(source[I_MUSIC]);
    alSourcePlay(source[I_WT]);
    alSourcePlay(source[I_CD]);
    alSourcePlay(source[I_FDD]);
    alSourcePlay(source[I_HDD]);
    alSourcePlay(source[I_YM2151]);
    alSourcePlay(source[I_CQM]);
    if (init_midi)
        alSourcePlay(source[I_MIDI]);

    if (sound_is_float) {
        if (init_midi)
            free(midi_buf);
        free(cd_buf);
        free(wt_buf);
        free(music_buf);
        free(buf);
        free(fdd_buf);
        free(hdd_buf);
        free(ym2151_buf);
        free(cqm_buf);
    } else {
        if (init_midi)
            free(midi_buf_int16);
        free(cd_buf_int16);
        free(wt_buf_int16);
        free(music_buf_int16);
        free(buf_int16);
        free(fdd_buf_int16);
        free(hdd_buf_int16);
        free(ym2151_buf_int16);
        free(cqm_buf_int16);
    }

    initialized = 1;
}

extern bool fast_forward;
void
givealbuffer_common(const void *buf, const uint8_t src, const int size, const int freq)
{
    int    processed;
    int    state;
    ALuint buffer;

    if (!initialized || fast_forward)
        return;

    alGetSourcei(source[src], AL_SOURCE_STATE, &state);

    if (state == 0x1014) {
        alSourcePlay(source[src]);
    }

    alGetSourcei(source[src], AL_BUFFERS_PROCESSED, &processed);
    if (processed >= 1) {
        const double gain = (sound_muted) ? 0.0 : pow(10.0, (double) sound_gain / 20.0);
        alListenerf(AL_GAIN, (float) gain);

        alSourceUnqueueBuffers(source[src], 1, &buffer);

        if (sound_is_float)
            alBufferData(buffer, AL_FORMAT_STEREO_FLOAT32, buf, size * (int) sizeof(float), freq);
        else
            alBufferData(buffer, AL_FORMAT_STEREO16, buf, size * (int) sizeof(int16_t), freq);

        alSourceQueueBuffers(source[src], 1, &buffer);
    }
}

void
givealbuffer(const void *buf)
{
    givealbuffer_common(buf, I_NORMAL, (sound_sample_rate / 50) << 1, sound_sample_rate);
}

void
givealbuffer_music(const void *buf)
{
    givealbuffer_common(buf, I_MUSIC, MUSICBUFLEN << 1, MUSIC_FREQ);
}

void
givealbuffer_wt(const void *buf)
{
    givealbuffer_common(buf, I_WT, WTBUFLEN << 1, WT_FREQ);
}

void
givealbuffer_cd(const void *buf)
{
    givealbuffer_common(buf, I_CD, CD_BUFLEN << 1, CD_FREQ);
}

void
givealbuffer_midi(const void *buf, const uint32_t size)
{
    givealbuffer_common(buf, I_MIDI, (int) size, midi_freq);
}

void
givealbuffer_fdd(const void *buf, const uint32_t size)
{
    givealbuffer_common(buf, I_FDD, (int) size, sound_sample_rate);
}

void
givealbuffer_hdd(const void *buf, const uint32_t size)
{
    givealbuffer_common(buf, I_HDD, (int) size, sound_sample_rate);
}

void
givealbuffer_ym2151(const void *buf)
{
    givealbuffer_common(buf, I_YM2151, YM2151BUFLEN << 1, YM2151_FREQ);
}

void
givealbuffer_cqm(const void *buf)
{
    givealbuffer_common(buf, I_CQM, CQMBUFLEN << 1, CQM_FREQ);
}
