#include <stdio.h>

#include "runtime/config.h"
#include "runtime/ds.h"
#include "runtime/mem_arena.h"
#include "runtime/runtime.h"

#include "gfx/gfx.h"
#include "gfx/render_list.h"
#include "gfx/renderer_api.h"

extern rt_cvar rt_Renderer;

/* Check basic relative pointer behaviour */
static rt_result RelPtrTest(void) {
    char buf[sizeof(rt_relptr) + sizeof(unsigned int)];
    rt_relptr *ptr       = (rt_relptr *)buf;
    unsigned int *target = (unsigned int *)&buf[sizeof(rt_relptr)];
    *target              = 42;
    rtSetRelptr(ptr, target);

    void *resolved = rtResolveRelptr(ptr);
    if ((uintptr_t)resolved != (uintptr_t)target)
        return 1;

    if (*(unsigned int *)resolved != *target)
        return 2;

    return RT_SUCCESS;
}

static rt_result NegRelPtrTest(void) {
    char buf[sizeof(unsigned int) + sizeof(rt_relptr)];
    unsigned int *target = (unsigned int *)buf;
    rt_relptr *ptr       = (rt_relptr *)&buf[sizeof(unsigned int)];
    *target              = 42;
    rtSetRelptr(ptr, target);

    void *resolved = rtResolveRelptr(ptr);
    if ((uintptr_t)resolved != (uintptr_t)target)
        return 1;

    if (*(unsigned int *)resolved != *target)
        return 2;

    return RT_SUCCESS;
}

static rt_result SetupRenderListFixture(void) {
    rt_result res = rtInitRuntime();
    if (res != RT_SUCCESS)
        return res;

    rt_Renderer.s = "null";

    rt_renderer_init_info info = {0};
    rtInitGFX(&info);
    return res;
}

static void TeardownRenderListFixture(void) {
    rtShutdownGFX();
    rtShutdownRuntime();
}

/* Check render list interface */
static rt_result PushRenderList(void) {
    typedef struct {
        int a;
        float b;
    } dummy_type;
    rt_render_object_type type = rtRegisterRenderObjectType(sizeof(dummy_type), "DummyType");

    rt_create_render_list_result list_res = rtCreateRenderList(type, 43);
    if (!list_res.ok) {
        return RT_INVALID_VALUE;
    }
    rt_render_list list = list_res.list;
    dummy_type dummy    = {42, 21.f};
    rtPushRenderListEntry(&list, &dummy);

    dummy_type check = RT_GET_RENDER_LIST_ELEMENT(list, dummy_type, 0);

    if (check.a != dummy.a || check.b != dummy.b)
        return RT_INVALID_VALUE;

    return RT_SUCCESS;
}

static rt_result PushLongRenderList(void) {
    typedef struct {
        float p[3];
        float v[3];
    } dummy_type;
    rt_render_object_type type = rtRegisterRenderObjectType(sizeof(dummy_type), "DummyType");

    rt_create_render_list_result list_res = rtCreateRenderList(type, 43);
    if (!list_res.ok)
        return RT_INVALID_VALUE;
    rt_render_list list = list_res.list;
    for (uint32_t i = 0; i < 512; ++i) {
        dummy_type dummy;
        for (int j = 0; j < 3; ++j)
            dummy.v[j] = dummy.p[j] = (float)i;
        rtPushRenderListEntry(&list, &dummy);

        dummy_type check = RT_GET_RENDER_LIST_ELEMENT(list, dummy_type, i);
        for (int j = 0; j < 3; ++j) {
            if (check.p[j] != (float)i || check.v[j] != (float)i)
                return RT_INVALID_VALUE;
        }
    }

    for (uint32_t i = 0; i < 512; ++i) {
        dummy_type check = RT_GET_RENDER_LIST_ELEMENT(list, dummy_type, i);
        for (int j = 0; j < 3; ++j) {
            if (check.p[j] != (float)i || check.v[j] != (float)i)
                return RT_INVALID_VALUE;
        }
    }
    return RT_SUCCESS;
}

static rt_result HashTableBasics(void) {
    {
        uint64_t mem[128];
        rt_hashtable ht = rtCreateHashtable(64, mem, NULL, NULL);

        for (uint64_t i = 0; i < 64; ++i) {
            if (rtHashtableInsert(&ht, i, i) != RT_SUCCESS)
                return RT_UNKNOWN_ERROR;
            uint64_t found = rtHashtableLookup(&ht, i, UINT64_MAX);
            if (found != i)
                return RT_INVALID_VALUE;
        }
    }

    {
        rt_create_arena_result arena_res = rtCreateArena(NULL, RT_KB(4));
        if (!arena_res.ok)
            return RT_OUT_OF_MEMORY;
        rt_arena arena = arena_res.arena;
        void *mem       = rtArenaPush(&arena, RT_HASH_TABLE_MEMORY_REQUIRED(64));
        if (!mem)
            return RT_OUT_OF_MEMORY;
        rt_hashtable ht = rtCreateHashtable(64, mem, rtHashtableGrowMemoryFromArena, &arena);
        for (uint64_t i = 0; i < 64; ++i) {
            if (rtHashtableInsert(&ht, 256+i, i) != RT_SUCCESS)
                return RT_UNKNOWN_ERROR;
            uint64_t found = rtHashtableLookup(&ht, 256+i, UINT64_MAX);
            if (found != i)
                return RT_INVALID_VALUE;
        }
        rtReleaseArena(&arena);
    }
    return RT_SUCCESS;
}

/* Scaffolding
 *
 * Run all the test cases, output if they passed or failed.
 */

typedef rt_result rt_fixture_setup_fn(void);
typedef void rt_fixture_teardown_fn(void);

typedef struct {
    const char *name;
    bool is_initialized;
    rt_fixture_setup_fn *setup;
    rt_fixture_teardown_fn *teardown;
} rt_test_fixture;

typedef rt_result rt_test_fnc(void);

typedef struct {
    const char *name;
    rt_test_fnc *fnc;
    rt_test_fixture *fixture;
} rt_test_case;

#define TEST_CASE(fn)                                                                              \
    { .name = #fn, .fnc = fn, .fixture = NULL, }

#define TEST_CASE_FIXTURE(fn, _fixture)                                                            \
    { .name = #fn, .fnc = fn, .fixture = &_fixture }

/* X-Macro to create named fixtures */

/* Add more fixtures here*/
#define TEST_FIXTURE_LIST                                                                          \
    TEST_FIXTURE(render_list_fixture, SetupRenderListFixture, TeardownRenderListFixture)

#define TEST_FIXTURE(n, setup_fn, teardown_fn)                                                     \
    rt_test_fixture n = {.name           = #n,                                                     \
                         .is_initialized = false,                                                  \
                         .setup          = setup_fn,                                               \
                         .teardown       = teardown_fn};
TEST_FIXTURE_LIST

#undef TEST_FIXTURE
#define TEST_FIXTURE(n, _s, _t) &n

static rt_test_fixture *_test_fixtures[] = {TEST_FIXTURE_LIST};

static rt_test_case _test_cases[] = {TEST_CASE(RelPtrTest),
                                     TEST_CASE(NegRelPtrTest),
                                     TEST_CASE_FIXTURE(PushRenderList, render_list_fixture),
                                     TEST_CASE_FIXTURE(PushLongRenderList, render_list_fixture),
                                     TEST_CASE(HashTableBasics)};

int main() {
    int out = 0;

    for (size_t i = 0; i < RT_ARRAY_COUNT(_test_cases); ++i) {
        if (_test_cases[i].fixture) {
            rt_test_fixture *fixture = _test_cases[i].fixture;
            if (!fixture->is_initialized) {
                printf("[SETUP %s] ... ", fixture->name);
                rt_result res = fixture->setup();
                if (res == RT_SUCCESS) {
                    printf("OK\n");
                    fixture->is_initialized = true;
                } else {
                    printf("FAILED (%u)\n", res);
                    ++out;
                    printf("Cannot run test case %s because the setup of fixture %s failed!\n",
                           _test_cases[i].name,
                           fixture->name);
                    continue;
                }
            }
        }
        printf("[%s] ... ", _test_cases[i].name);
        rt_result res = _test_cases[i].fnc();
        if (res == RT_SUCCESS) {
            printf("OK\n");
        } else {
            printf("FAILED (%u)\n", res);
            ++out;
        }
    }

    /* Teardown fixtures */
    for (size_t i = 0; i < RT_ARRAY_COUNT(_test_fixtures); ++i) {
        if (_test_fixtures[i]->is_initialized) {
            printf("[TEARDOWN %s] ... ", _test_fixtures[i]->name);
            _test_fixtures[i]->teardown();
            printf("DONE\n");
        }
    }

    return out;
}
