#include "twn_draw_c.h"
#include "twn_draw.h"
#include "twn_util.h"
#include "twn_util_c.h"
#include "twn_engine_context_c.h"

#include <stb_truetype.h>
#include <stb_ds.h>

#define ASCII_START 32
#define ASCII_END 128
#define NUM_DISPLAY_ASCII ((ASCII_END - ASCII_START) + 1)

/* should be large enough that there's virtually never more than one block */
#define STRING_ARENA_BLOCK_SIZE 512000


typedef struct StringArenaBlock {
    struct StringArenaBlock *next;
    char *buffer;
    size_t used;
} StringArenaBlock;


typedef struct StringArena {
    StringArenaBlock *head;
    StringArenaBlock *current_block;
    char *ptr;
} StringArena;

static StringArena string_arena;


typedef struct FontFileBuffer {
    size_t len;
    unsigned char *buffer;
} FontFileBuffer;

typedef struct FontFileCacheItem {
    char *key;
    FontFileBuffer value;
} FontFileCacheItem;

static FontFileCacheItem *font_file_cache_hash;


static void string_arena_init(StringArena *arena) {
    arena->head = cmalloc(sizeof *arena->head);
    arena->current_block = arena->head;
    arena->ptr = arena->current_block->buffer;

    arena->current_block->next = NULL;
    arena->current_block->buffer = cmalloc(STRING_ARENA_BLOCK_SIZE);
    arena->current_block->used = 0;
}


static void string_arena_deinit(StringArena *arena) {
    StringArenaBlock *p = arena->head;
    while (p != NULL) {
        StringArenaBlock *block = p;
        p = block->next; /* need to keep this before freeing */

        SDL_free(block->buffer);
        SDL_free(block);
    }
}


static void string_arena_reset(StringArena *arena) {
    arena->current_block = arena->head;
    arena->current_block->used = 0;
    arena->ptr = arena->current_block->buffer;
}


static char *string_arena_alloc(StringArena *arena, size_t size) {
    /* will never fit! */
    if (size > STRING_ARENA_BLOCK_SIZE) {
        /* is there a more graceful way to handle this without risking some sort of horror? */
        CRY("String arena allocation failure", "Tried to allocate a size larger than the block size");
        die_abruptly();
    }

    /* won't fit, we need a new block first */
    if (arena->current_block->used + size > STRING_ARENA_BLOCK_SIZE) {
        log_info("String arena block size exceeded, moving on to the next...");

        /* only allocate if there wasn't one ready to go already */
        if (arena->current_block->next == NULL) {
            log_info("Allocating string arena block...");

            arena->current_block->next = cmalloc(sizeof *arena->head);
            arena->current_block->next->next = NULL;
            arena->current_block->next->buffer = cmalloc(STRING_ARENA_BLOCK_SIZE);
        }

        arena->current_block = arena->current_block->next;
        arena->current_block->used = 0;
        arena->ptr = arena->current_block->buffer;
    }

    char *chunk = arena->ptr;
    arena->ptr += size;
    arena->current_block->used += size * (sizeof *arena->ptr);
    return chunk;
}


static FontData *text_load_font_data(const char *path, int height_px) {
    FontData *font_data = ccalloc(1, sizeof *font_data);
    font_data->file_path = path;
    font_data->height_px = height_px;

    unsigned char* bitmap = ccalloc(ctx.font_texture_size * ctx.font_texture_size, 1);

    {
        unsigned char *buf = NULL;
        int64_t buf_len = 0;

        /* if the file was already loaded just get it */
        FontFileCacheItem *font_file_ptr = shgetp_null(font_file_cache_hash, path);
        if (font_file_ptr != NULL) {
            buf = font_file_ptr->value.buffer;
            buf_len = font_file_ptr->value.len;
        } else {
            buf_len = file_to_bytes(path, &buf);
            FontFileBuffer buffer = { buf_len, buf };
            shput(font_file_cache_hash, path, buffer);
        }

        stbtt_InitFont(&font_data->info, buf, stbtt_GetFontOffsetForIndex(buf, 0));
        
        /* might as well get these now, for later */
        font_data->scale_factor = stbtt_ScaleForPixelHeight(&font_data->info, (float)height_px);
        stbtt_GetFontVMetrics(
            &font_data->info,
            &font_data->ascent,
            &font_data->descent,
            &font_data->line_gap
        );
        font_data->ascent = (int)((float)font_data->ascent * font_data->scale_factor);
        font_data->descent = (int)((float)font_data->descent * font_data->scale_factor);
        font_data->line_gap = (int)((float)font_data->line_gap * font_data->scale_factor);

        stbtt_pack_context pctx;
        stbtt_PackBegin(&pctx, bitmap, (int)ctx.font_texture_size, (int)ctx.font_texture_size, 0, 1, NULL);
        stbtt_PackSetOversampling(&pctx, (unsigned int)ctx.font_oversampling, (unsigned int)ctx.font_oversampling);
        stbtt_PackFontRange(&pctx, buf, 0, (float)height_px, ASCII_START, NUM_DISPLAY_ASCII, font_data->char_data);
        stbtt_PackEnd(&pctx);
    }

    font_data->texture = create_gpu_texture(ctx.font_filtering, true);
    upload_gpu_texture(
        font_data->texture,
        bitmap,
        1,
        (int)ctx.font_texture_size,
        (int)ctx.font_texture_size
    );
    SDL_free(bitmap);

    return font_data;
}


static void text_destroy_font_data(FontData *font_data) {
    delete_gpu_texture(font_data->texture);
    SDL_free(font_data);
}


static void text_draw_with(FontData* font_data, char* text, Vec2 position, Color color) {
    VertexBuffer vertex_array = 0;
    if (vertex_array == 0)
        vertex_array = create_vertex_buffer();

    const size_t len = SDL_strlen(text);

    VertexBufferBuilder payload = build_vertex_buffer(vertex_array, get_text_payload_size() * len);

    for (size_t i = 0; i < len; ++i) {
        const char c = text[i];

        /* outside the range of what we want to display */
        //if (c < ASCII_START || c > ASCII_END)
        if (c < ASCII_START)
            continue;

        /* stb_truetype.h conveniently provides everything we need to draw here! */
        stbtt_aligned_quad quad;
        stbtt_GetPackedQuad(
            font_data->char_data,
            (int)ctx.font_texture_size,
            (int)ctx.font_texture_size,
            c - ASCII_START,
            &position.x,
            &position.y,
            &quad,
            true
        );

        /* have to do this so the "origin" is at the top left */
        /* maybe there's a better way, or maybe this isn't a good idea... */
        /* who knows */
        quad.y0 += (float)font_data->ascent;
        quad.y1 += (float)font_data->ascent;

        push_text_payload_to_vertex_buffer_builder(font_data, &payload, quad);
    }

    finally_draw_text(font_data, len, color, vertex_array);
}


static void ensure_font_cache(const char *font_path, int height_px) {
    /* HACK: don't */
    bool is_cached = false;
    for (size_t i = 0; i < arrlenu(ctx.text_cache.data); ++i) {
        FontData *font_data = ctx.text_cache.data[i];
        if ((SDL_strcmp(font_path, font_data->file_path) == 0) && height_px == font_data->height_px) {
            is_cached = true;
            break;
        }
    }
    if (!is_cached) {
        FontData *new_font_data = text_load_font_data(font_path, height_px);
        arrput(ctx.text_cache.data, new_font_data);
    }
}


static FontData *get_font_data(const char *font_path, int height_px) {
    FontData *font_data = NULL;
    for (size_t i = 0; i < arrlenu(ctx.text_cache.data); ++i) {
        font_data = ctx.text_cache.data[i];
        if ((SDL_strcmp(font_path, font_data->file_path) == 0) && height_px == font_data->height_px) {
            break;
        }
    }
    return font_data;
}


void render_text(const TextPrimitive *text) {
    FontData *font_data = get_font_data(text->font, text->height_px);
    text_draw_with(font_data, text->text, text->position, text->color);
}


void text_cache_init(TextCache *cache) {
    arrsetlen(cache->data, 0);
    string_arena_init(&string_arena);

    sh_new_arena(font_file_cache_hash);
}


void text_cache_deinit(TextCache *cache) {
    for (size_t i = 0; i < arrlenu(ctx.text_cache.data); ++i) {
        text_destroy_font_data(ctx.text_cache.data[i]);
    }

    for (size_t i = 0; i < shlenu(font_file_cache_hash); ++i) {
        SDL_free(font_file_cache_hash[i].value.buffer);
    }
    shfree(font_file_cache_hash);

    string_arena_deinit(&string_arena);
    arrfree(cache->data);
}


void text_cache_reset_arena(TextCache *cache) {
    (void)cache;
    string_arena_reset(&string_arena);
}


void draw_text(const char *string, Vec2 position, int height_px, Color color, const char *font_path) {
    ensure_font_cache(font_path, height_px);

    /* the original string might not be around by the time it's used, so copy it */
    size_t str_length = SDL_strlen(string) + 1;
    if (str_length == 1)
        /* empty string */
        return;
    char *dup_string = string_arena_alloc(&string_arena, str_length);
    SDL_strlcpy(dup_string, string, str_length);

    TextPrimitive text = {
        .color = color,
        .position = position,
        .text = dup_string,
        .font = font_path,
        .height_px = height_px,
    };

    Primitive2D primitive = {
        .type = PRIMITIVE_2D_TEXT,
        .text = text,
    };

    arrput(ctx.render_queue_2d, primitive);
}


int draw_text_width(const char *string, int height_px, const char *font_path) {
    ensure_font_cache(font_path, height_px);
    FontData *font_data = get_font_data(font_path, height_px);

    int length = 0;
    for (const char *p = string; *p != '\0'; ++p) {
        const char c = *p;
        int advance_width = 0;
        int left_side_bearing = 0;
        stbtt_GetCodepointHMetrics(&font_data->info, (int)c, &advance_width, &left_side_bearing);
        length += advance_width;
    }
    
    return (int)((float)length * font_data->scale_factor);
}
