#include "townengine/audio/internal_api.h"
#include "townengine/config.h"
#include "townengine/context.h"
#include "townengine/util.h"

#include <SDL2/SDL.h>
#include <stb_ds.h>
#include <physfs.h>

#define STB_VORBIS_HEADER_ONLY
#include <stb_vorbis.c>

#include <stdint.h>
#include <string.h>

/* TODO: default to float sampling format? */

static const char *audio_exts[audio_file_type_count] = {
    ".ogg", /* audio_file_type_ogg */
    ".xm",  /* audio_file_type_xm */
};


/* TODO: count frames without use, free the memory when threshold is met */
/* TODO: count repeated usages for sound effect cases with rendering to ram? */
/* stores path to data hash, useful for sound effects */
static struct audio_file_cache {
    char *key;
    struct audio_file_cache_value {
        unsigned char *data;
        size_t len;
    } value;
} *audio_file_cache;


static int64_t get_audio_data(const char *path, unsigned char **data) {
    const struct audio_file_cache *cache = shgetp_null(audio_file_cache, path);
    if (!cache) {
        unsigned char *file;
        int64_t len = file_to_bytes(path, &file);
        if (len == -1) {
            CRY("Audio error", "Error reading file");
            return -1;
        }

        const struct audio_file_cache_value value = { file, (size_t)len };
        shput(audio_file_cache, path, value);

        *data = file;
        return len;
    }

    *data = cache->value.data;
    return (int64_t)cache->value.len;
}


void play_audio(const char *path, const char *channel) {
    const struct audio_channel_item *pair = shgetp_null(ctx.audio_channels, channel);
    if (!pair)
        play_audio_ex(path, channel, get_default_audio_args());
    else
        play_audio_ex(path, channel, pair->value.args);
}


static t_audio_file_type infer_audio_file_type(const char *path) {
    size_t path_len = strlen(path);

    for (int i = 0; i < audio_file_type_count; ++i) {
        size_t ext_length = strlen(audio_exts[i]);
        if (path_len <= ext_length)
            continue;

        if (strcmp(&path[path_len - ext_length], audio_exts[i]) == 0)
            return (t_audio_file_type)i;
    }

    return audio_file_type_unknown;
}


/* TODO: error propagation and clearing of resources on partial success? */
/*       or should we expect things to simply fail? */
static union audio_context init_audio_context(const char *path, t_audio_file_type type) {
    switch (type) {
    case audio_file_type_ogg: {
        unsigned char *data;
        int64_t len = get_audio_data(path, &data);
        if (len == -1) {
            CRY("Audio error", "Error reading file");
            break;
        }

        int error = 0;
        stb_vorbis* handle = stb_vorbis_open_memory(data, (int)len, &error, NULL);
        if (error != 0) {
            CRY("Audio error", "Error reading .ogg file");
            break;
        }

        stb_vorbis_info info = stb_vorbis_get_info(handle);

        return (union audio_context) {
            .vorbis = {
                .data = data,
                .handle = handle,
                .frequency = info.sample_rate,
                .channel_count = (uint8_t)info.channels,
            }
        };
    }

    case audio_file_type_xm: {
        unsigned char *data;
        int64_t len = get_audio_data(path, &data);
        if (len == -1) {
            CRY("Audio error", "Error reading file");
            break;
        }

        xm_context_t *handle;
        int response = xm_create_context_safe(&handle,
                                             (const char *)data,
                                             (size_t)len,
                                             AUDIO_FREQUENCY);
        if (response != 0) {
            CRY("Audio error", "Error loading xm module");
            break;
        }

        xm_set_max_loop_count(handle, 1);

        return (union audio_context) {
            .xm = { .handle = handle }
        };
    }

    default:
        CRY("Audio error", "Unhandled audio format (in init)");
        return (union audio_context){0};
    }

    return (union audio_context){0};
}


static void repeat_audio(struct audio_channel *channel) {
    switch (channel->file_type) {
    case audio_file_type_ogg: {
        stb_vorbis_seek_start(channel->context.vorbis.handle);
        break;
    }

    case audio_file_type_xm: {
        xm_restart(channel->context.xm.handle);
        break;
    }

    default:
        CRY("Audio error", "Unhandled audio format (in repeat)");
        break;
    }
}


void play_audio_ex(const char *path, const char *channel, t_play_audio_args args) {
    struct audio_channel_item *pair = shgetp_null(ctx.audio_channels, channel);

    /* create a channel if it doesn't exist */
    if (!pair) {
        t_audio_file_type file_type = infer_audio_file_type(path);
        struct audio_channel new_channel = {
            .args = args,
            .file_type = file_type,
            .context = init_audio_context(path, file_type),
            .path = path,
            .name = channel,
        };
        shput(ctx.audio_channels, channel, new_channel);
        pair = shgetp_null(ctx.audio_channels, channel);
    }

    /* TODO: destroy and create new context when channel is reused for different file */

    /* works for both restarts and new audio */
    if (strcmp(pair->value.path, path) == 0)
        repeat_audio(&pair->value);
}


t_play_audio_args *get_audio_args(const char *channel) {
    struct audio_channel_item *pair = shgetp_null(ctx.audio_channels, channel);
    if (!pair)
        return NULL;

    return &pair->value.args;
}


t_play_audio_args get_default_audio_args(void) {
    return (t_play_audio_args){
        .repeat = false,
        .crossfade = false,
        .volume = 1.0f,
        .panning = 0.0f,
    };
}


/* this assumes int16_t based streams */
static void audio_mixin_streams(const struct audio_channel *channel,
                                uint8_t *restrict a,
                                uint8_t *restrict b,
                                size_t frames)
{
    int16_t *const sa = (int16_t *)a;
    int16_t *const sb = (int16_t *)b;

    const float left_panning = fminf(fabsf(channel->args.panning - 1.0f), 1.0f);
    const float right_panning = fminf(fabsf(channel->args.panning + 1.0f), 1.0f);

#if AUDIO_N_CHANNELS == 2

    for (size_t s = 0; s < frames; s += 2) {
        /* left channel */
        sa[s] += (int16_t)(sb[s] * channel->args.volume * left_panning);

        /* right channel */
        sa[s + 1] += (int16_t)(sb[s + 1] * channel->args.volume * right_panning);
    }
#else
#error "Unimplemented channel count"
#endif
}


/* remember: sample is data for all channels where frame is a part of it */
static void audio_sample_and_mixin_channel(const struct audio_channel *channel,
                                           uint8_t *stream,
                                           int len)
{
    static uint8_t buffer[16384];
    const int int16_buffer_frames = sizeof (buffer) / sizeof (int16_t);
    const int float_buffer_frames = sizeof (buffer) / sizeof (float);
    const int stream_frames = len / (int)(sizeof (int16_t));

    switch (channel->file_type) {
    case audio_file_type_ogg: {
        /* feed stream for needed conversions */
        for (int i = 0; i < stream_frames; ) {
            const int n_frames = (stream_frames - i) > int16_buffer_frames ?
                int16_buffer_frames : stream_frames - i;

            const int samples_per_channel = stb_vorbis_get_samples_short_interleaved(
                channel->context.vorbis.handle,
                channel->context.vorbis.channel_count,
                (int16_t *)buffer,
                n_frames);

            /* handle end of file */
            if (samples_per_channel == 0) {
                if (channel->args.repeat) {
                    /* seek to start and try sampling some more */
                    stb_vorbis_seek_start(channel->context.vorbis.handle);
                    continue;
                } else
                    /* leave silence */
                    break;
            }

            /* panning and mixing */
            audio_mixin_streams(channel,
                                &stream[i * sizeof(int16_t)], buffer,
                                samples_per_channel * 2);

            i += samples_per_channel * 2;
        }

        break;
    }

    case audio_file_type_xm: {
        for (int i = 0; i < stream_frames; ) {
            const int n_frames = (stream_frames - i) > float_buffer_frames ?
                float_buffer_frames : stream_frames - i;

            const int samples_per_channel = xm_generate_samples(channel->context.xm.handle,
                                                                (float *)buffer,
                                                                n_frames / 2);

            /* handle end of file */
            if (samples_per_channel == 0) {
                if (channel->args.repeat) {
                    /* seek to start and try sampling some more */
                    xm_restart(channel->context.xm.handle);
                    continue;
                } else
                    /* leave silence */
                    break;
            }

            /* convert floats to int16_t */
            for (int p = 0; p < samples_per_channel * 2; ++p)
                ((int16_t *)buffer)[p] = (int16_t)(((float *)buffer)[p] * (float)INT16_MAX);

            /* panning and mixing */
            audio_mixin_streams(channel,
                                &stream[i * sizeof(int16_t)],
                                buffer,
                                samples_per_channel * 2);

            i += samples_per_channel * 2;
        }

        break;
    }

    default:
        CRY("Audio error", "Unhandled audio format (in sampling)");
        break;
    }
}


static void sanity_check_channel(const struct audio_channel *channel) {
    if (channel->args.volume < 0.0f || channel->args.volume > 1.0f)
        log_warn("Volume argument is out of range for channel (%s)", channel->name);

    if (channel->args.panning < -1.0f || channel->args.panning > 1.0f)
        log_warn("panning argument is out of range for channel (%s)", channel->name);
}


void audio_callback(void *userdata, uint8_t *stream, int len) {
    (void)userdata;

    /* prepare for mixing */
    SDL_memset(stream, 0, len);

    for (int i = 0; i < shlen(ctx.audio_channels); ++i) {
        sanity_check_channel(&ctx.audio_channels[i].value);
        audio_sample_and_mixin_channel(&ctx.audio_channels[i].value, stream, len);
    }
}
