#include "sound.h"
#include "arena.h"
#include "log.h"
#include "sys.h"
#include "scratch.h"
#include "resource.h"
#include "asset_cache.h"
#include "mp3.h"
#include "work.h"

struct sound_task_params {
    struct sound_task_params *next_free;

    u32 flags;
    struct asset *asset;
    u64 path_len;
    char path_cstr[1024];
};

struct sound_task_params_store {
    struct sound_task_params *head_free;
    struct arena arena;
    struct sys_mutex mutex;
};

/* ========================== *
 * Global state
 * ========================== */

GLOBAL struct {
    struct sound_task_params_store params;
} G = { 0 }, DEBUG_ALIAS(G, G_sound);

/* ========================== *
 * Startup
 * ========================== */

struct sound_startup_receipt sound_startup(struct work_startup_receipt *work_sr,
                                           struct asset_cache_startup_receipt *asset_cache_sr,
                                           struct resource_startup_receipt *resource_sr)
{
    (UNUSED)work_sr;
    (UNUSED)asset_cache_sr;
    (UNUSED)resource_sr;

    G.params.arena = arena_alloc(GIGABYTE(64));
    G.params.mutex = sys_mutex_alloc();

    return (struct sound_startup_receipt) { 0 };
}

/* ========================== *
 * Load task param store
 * ========================== */

INTERNAL struct sound_task_params *sound_task_params_alloc(void)
{
    struct sound_task_params *p = NULL;
    {
        sys_mutex_lock(&G.params.mutex);
        {
            if (G.params.head_free) {
                p = G.params.head_free;
                G.params.head_free = p->next_free;
            } else {
                p = arena_push_zero(&G.params.arena, struct sound_task_params);
            }
        }
        sys_mutex_unlock(&G.params.mutex);
    }
    return p;
}

INTERNAL void sound_task_params_release(struct sound_task_params *p)
{
    sys_mutex_lock(&G.params.mutex);
    {
        p->next_free = G.params.head_free;
        G.params.head_free = p;
    }
    sys_mutex_unlock(&G.params.mutex);
}

/* ========================== *
 * Load
 * ========================== */

INTERNAL WORK_TASK_FUNC_DEF(sound_load_asset_task, vparams)
{
    __prof;
    struct sound_task_params *params = (struct sound_task_params *)vparams;
    struct temp_arena scratch = scratch_begin_no_conflict();
    struct string path = string_from_cstr_len(params->path_cstr, params->path_len);
    struct asset *asset = params->asset;
    u32 flags = params->flags;

    logf_info("Loading sound \"%F\"", FMT_STR(path));
    sys_timestamp_t start_ts = sys_timestamp();

    b32 success = true;
    struct string error_msg = STR("Unknown error");

    ASSERT(string_ends_with(path, STR(".mp3")));
    if (resource_exists(path)) {
        u64 decode_flags = 0;
        if (flags & SOUND_FLAG_STEREO) {
            decode_flags |= MP3_DECODE_FLAG_STEREO;
        }

        /* Decode */
        struct resource sound_rs = resource_open(path);
        struct mp3_decode_result decoded = mp3_decode(scratch.arena, sound_rs.bytes, decode_flags);
        resource_close(sound_rs);

        if (!decoded.success) {
            success = false;
            error_msg = STR("Failed to decode sound file");
            goto abort;
        }

        /* Store */
        struct sound *sound = NULL;
        i16 *samples = NULL;
        {
            struct asset_cache_store store = asset_cache_store_open();
            sound = arena_push(store.arena, struct sound);
            samples = arena_push_array(store.arena, i16, decoded.pcm.count);
            asset_cache_store_close(&store);
        }

        /* Initialize */
        *sound = (struct sound) {
            .flags = flags,
            .pcm.count = decoded.pcm.count,
            .pcm.samples = samples
        };
        MEMCPY(sound->pcm.samples, decoded.pcm.samples, decoded.pcm.count * sizeof(*decoded.pcm.samples));

        logf_info("Finished loading sound \"%F\" in %F seconds",
                  FMT_STR(path),
                  FMT_FLOAT(sys_timestamp_seconds(sys_timestamp() - start_ts)));

        asset_cache_mark_ready(asset, sound);
    } else {
        success = false;
        error_msg = STR("Resource not found");
        goto abort;
    }

    abort:

    if (!success) {
        logf_error("Error loading sound \"%F\": %F", FMT_STR(path), FMT_STR(error_msg));

        /* Store */
        struct sound *sound = NULL;
        {
            struct asset_cache_store store = asset_cache_store_open();
            sound = arena_push(store.arena, struct sound);
            asset_cache_store_close(&store);
        }
        *sound = (struct sound) { 0 };

        asset_cache_mark_ready(asset, sound);
    }

    sound_task_params_release(params);

    scratch_end(scratch);
}

struct asset *sound_load_asset(struct string path, u32 flags, b32 help)
{
    __prof;
    struct temp_arena scratch = scratch_begin_no_conflict();

    /* Generate and append sound flags to path key */
    struct string key = string_format(scratch.arena,
                                      STR("%F%F_sound"),
                                      FMT_STR(path),
                                      FMT_UINT((u64)flags));
    u64 hash = asset_cache_hash(key);
    b32 is_first_touch;
    struct asset *asset = asset_cache_touch(key, hash, &is_first_touch);

    if (is_first_touch) {
        /* Assemble task params */
        struct sound_task_params *params = sound_task_params_alloc();
        if (path.len > (sizeof(params->path_cstr) - 1)) {
            sys_panic(string_format(scratch.arena,
                                    STR("Sound path \"%F\" too long!"),
                                    FMT_STR(path)));
        }
        cstr_buff_from_string(BUFFER_FROM_ARRAY(params->path_cstr), path);
        params->path_len = path.len;
        params->asset = asset;
        params->flags = flags;

        /* Push task */
        asset_cache_mark_loading(asset);
        struct work_handle wh = { 0 };
        if (help) {
            wh = work_push_task_and_help(&sound_load_asset_task, params, WORK_PRIORITY_NORMAL);
        } else {
            wh = work_push_task(&sound_load_asset_task, params, WORK_PRIORITY_NORMAL);
        }
        asset_cache_set_work(asset, &wh);
    }

    scratch_end(scratch);
    return asset;
}

struct sound *sound_load_async(struct string path, u32 flags)
{
    __prof;
    struct asset *asset = sound_load_asset(path, flags, false);
    struct sound *sound = (struct sound *)asset_cache_get_store_data(asset);
    return sound;
}


struct sound *sound_load(struct string path, u32 flags)
{
    __prof;
    struct asset *asset = sound_load_asset(path, flags, true);
    asset_cache_wait(asset);
    struct sound *sound = (struct sound *)asset_cache_get_store_data(asset);
    return sound;
}
