#include "resources.h"
#include "render.h"
#include "stdbool.h"
#include "utils/hash_map.h"
#include "utils/dictionary.h"
#include "utils/debug.h"
#include "utils/strutil.h"

char const *RESOURCE_DIRECTORY = "resources";

typedef enum ResourceType {
    RESOURCE_MODEL, RESOURCE_TEXTURE, RESOURCE_SHADER,
    RESOURCE_MAX
} ResourceType;

typedef struct ResourceContainer {
    char const *path;
    char const *name;
    unsigned use_counter;
    bool is_loaded;
    ResourceType type;
    union {
        Model model;
        Texture texture;
        Shader shader;
    };
} ResourceContainer;

typedef void (*LoadResourceFn)(ResourceContainer *resource);
typedef void (*UnloadResourceFn)(ResourceContainer *resource);

static void Internal_LoadModelResource(ResourceContainer *resource);
static void Internal_UnloadModelResource(ResourceContainer *resource);
static void Internal_LoadTextureResource(ResourceContainer *resource);
static void Internal_UnloadTextureResource(ResourceContainer *resource);
static void Internal_LoadShaderResource(ResourceContainer *resource);
static void Internal_UnloadShaderResource(ResourceContainer *resource);

LoadResourceFn g_load_functions[] = {Internal_LoadModelResource, Internal_LoadTextureResource, Internal_LoadShaderResource};
UnloadResourceFn g_unload_functions[] = {Internal_UnloadModelResource, Internal_UnloadTextureResource, Internal_UnloadShaderResource};

static HashMap g_resource_map;
static FilePathList g_resource_files;

//! hash function for hashmaps with char* as key type.
static uintptr_t HashMapHashString(char const **str_ptr) {
    return strhash(*str_ptr);
}

//! Use file extensions to figure out the
static ResourceType ResourceGetTypeFromPath(char const *path) {
    // Texture files
    if(IsFileExtension(path, ".png"))  return RESOURCE_TEXTURE;
    if(IsFileExtension(path, ".jpg"))  return RESOURCE_TEXTURE;
    if(IsFileExtension(path, ".jpeg")) return RESOURCE_TEXTURE;
    // Model files
    if(IsFileExtension(path, ".glb"))  return RESOURCE_MODEL;
    if(IsFileExtension(path, ".gltf")) return RESOURCE_MODEL;
    // Shader files
    if(IsFileExtension(path, ".fg"))   return RESOURCE_SHADER;
    if(IsFileExtension(path, ".vs"))   return RESOURCE_SHADER;
    return RESOURCE_MAX;
}

//! Find which resource directory to use: Relative to working directory, or relative to application directory
//! Working directory resources are only supposed to be used for debugging.
static inline
void Internal_InitializeResourceDirectory() {
    // if there is a resource directory in the working directory, prioritize that for debugging
    if(DirectoryExists(RESOURCE_DIRECTORY)) {
        ChangeDirectory(TextFormat("%s/%s", GetWorkingDirectory(), RESOURCE_DIRECTORY));
        LOG_WARNING("Using working dir resources, this is intended for debug purposes only.");
        return;
    }
    // check application installation directory for a resource directory
    // this is the default running environment
    char const *installation_resource_dir = TextFormat("%s/%s", GetApplicationDirectory(), RESOURCE_DIRECTORY);
    if(DirectoryExists(installation_resource_dir)) {
        ChangeDirectory(installation_resource_dir);
        return;
    }
    UNREACHABLE("Failed to find a valid resource directory");
}

static inline
void Internal_IndexResourceDirectory() {
    g_resource_files = LoadDirectoryFilesEx(GetWorkingDirectory(), NULL, true);
    g_resource_map = hash_map_from_types(char const *, ResourceContainer, HashMapHashString);
    ResourceContainer placeholder;
    for(size_t i = 0; i < g_resource_files.count; ++i) {
        placeholder = (ResourceContainer){
            .is_loaded = false,
            .use_counter = 0,
            .path = g_resource_files.paths[i],
            .name = GetFileNameWithoutExt(g_resource_files.paths[i]),
            .type = ResourceGetTypeFromPath(g_resource_files.paths[i])
        };
        // only index resources that the engine knows how to load
        if(placeholder.type == RESOURCE_SHADER) { // shaders are made up of two files, so their paths are treated differently
            // if the shader has already been added, don't accidentally add it twice.
            ResourceContainer *found_container = hash_map_get_as(ResourceContainer, &g_resource_map, &placeholder.name);
            if(found_container == NULL) {
                placeholder.path = GetDirectoryPath(placeholder.path);
                LOG_INFO("Internal_IndexResourceDirectory: Indexing %s as %s", placeholder.path, placeholder.name);
                hash_map_insert(&g_resource_map, &placeholder.name, &placeholder);
                LOG_INFO("hash: %zu", strhash(placeholder.name));
            }
        } else if(placeholder.type != RESOURCE_MAX) { // regular resources
            LOG_INFO("Internal_IndexResourceDirectory: Indexing %s as %s", placeholder.path, placeholder.name);
            hash_map_insert(&g_resource_map, &placeholder.name, &placeholder);
            LOG_INFO("hash: %zu", strhash(placeholder.name));
        }
    }
}

void InitializeResourceSubsystem() {
    Internal_InitializeResourceDirectory();
    Internal_IndexResourceDirectory();
}

void CleanResourceSubsystem() {
    List resources = hash_map_values(&g_resource_map);
    list_foreach(ResourceContainer *,resource, &resources)
        if(resource->is_loaded)
            g_unload_functions[resource->type](resource);
    hash_map_empty(&g_resource_map);
    UnloadDirectoryFiles(g_resource_files);
}

bool GetModelResource(char const *path, ModelResource *out) {
    ResourceContainer *container = hash_map_get_as(ResourceContainer, &g_resource_map, &path);
    *out = ResourceEmpty(Model);
    // assert some assumptions about the found resource
    ASSERT_RETURN(container != NULL, false, "GetTextureResource: Resource %s not in index.", path);
    ASSERT_RETURN(container->type == RESOURCE_MODEL, false, "GetTextureResource: Resource %s is not a Texture.", path);
    ++container->use_counter;
    *out = (ModelResource) {
        .handle = container,
        .resource = &container->model
    };
    return true;
}

bool GetTextureResource(char const *path, TextureResource *out) {
    ResourceContainer *container = hash_map_get_as(ResourceContainer, &g_resource_map, &path);
    *out = ResourceEmpty(Texture);
    // assert some assumptions about the found resource
    ASSERT_RETURN(container != NULL, false, "GetTextureResource: Resource %s not in index.", path);
    ASSERT_RETURN(container->type == RESOURCE_TEXTURE, false, "GetTextureResource: Resource %s is not a Texture.", path);
    *out = (TextureResource) {
        .handle = container,
        .resource = &container->texture
    };
    return true;
}

bool GetShaderResource(char const *path, ShaderResource *out) {
    ResourceContainer *container = hash_map_get_as(ResourceContainer, &g_resource_map, &path);
    *out = ResourceEmpty(Shader);
    ASSERT_RETURN(container != NULL, false, "GetShaderResource: Resource %s not in index.", path);
    ASSERT_RETURN(container->type == RESOURCE_SHADER, false, "GetShaderResource: Resource %s is not a Shader.", path);
    *out = (ShaderResource) {
        .handle = container,
        .resource = &container->shader
    };
    return true;
}

bool IsResourceLoaded(ResourceHandle handle) {
    return handle->is_loaded;
}

void LoadResource(ResourceHandle handle) {
    ASSERT_RETURN(handle != NULL,, "LoadResource: Resource handle invalid");
    g_load_functions[handle->type](handle);
    handle->is_loaded = true;
    ++handle->use_counter;
}

void ReleaseResource(ResourceHandle handle) {
    ASSERT_RETURN(handle != NULL,, "ReleaseResource: Resource handle invalid");
    ASSERT_RETURN_WARN(handle->is_loaded,, "ReleaseResource: Resource %s is not loaded.", handle->path);
    g_unload_functions[handle->type](handle);
    handle->is_loaded = false;
    --handle->use_counter;
}

static
void Internal_LoadModelResource(ResourceContainer *resource) {
    LOG_INFO("Loading model from: %s", resource->path);
    resource->model = LoadModel(resource->path);
}

static
void Internal_UnloadModelResource(ResourceContainer *resource) {
    UnloadModel(resource->model);
    resource->model = (Model){0};
}

static
void Internal_LoadTextureResource(ResourceContainer *resource) {
    resource->texture = LoadTexture(resource->path);
}

static
void Internal_UnloadTextureResource(ResourceContainer *resource) {
    UnloadTexture(resource->texture);
    resource->texture = (Texture){0};
}

static
void Internal_LoadShaderResource(ResourceContainer *resource) {
    resource->shader = LoadShader(TextFormat("%s/%s.vs", resource->path, resource->name), TextFormat("%s/%s.fs", resource->path, resource->name));
    ASSERT_RETURN(IsShaderReady(resource->shader),, "Internal_LoadShaderResource: Shader failed to load.");
    Internal_OnShaderLoaded((ShaderResource){.resource = &resource->shader, .handle = resource});
}

static
void Internal_UnloadShaderResource(ResourceContainer *resource) {
    UnloadShader(resource->shader);
    resource->shader = (Shader){0};
    Internal_OnShaderUnloaded((ShaderResource){.resource = &resource->shader, .handle = resource});
}

