#include "../../include/mgl/graphics/text.h"
#include "../../include/mgl/graphics/font.h"
#include "../../include/mgl/system/utf8.h"
#include "../../include/mgl/window/window.h"
#include "../../include/mgl/mgl.h"
#include <stdio.h>

/* TODO: Cache mgl_font_get_glyph */

#define TAB_WIDTH 4

typedef struct {
    size_t codepoint_index;
    uint32_t codepoint;
    float kerning;
    float glyph_width;
    const mgl_font_glyph *glyph;
    mgl_vec2f glyph_offset;
} codepoint_loop_callback_data;

static float max_float(float a, float b) {
    return a >= b ? a : b;
}

static float float_abs(float value) {
    return value >= 0.0f ? value : -value;
}

/* Return false to stop the loop. |glyph| is not valid if codepoint is '\n' */
typedef bool (*codepoint_loop_callback)(codepoint_loop_callback_data callback_data, void *userdata);
static void mgl_text_for_each_codepoint(const mgl_text *self, codepoint_loop_callback callback, void *userdata) {
    mgl_font_glyph glyph;
    size_t codepoint_index = 0;
    float kerning = 0.0f;
    float glyph_width = 0.0f;
    uint32_t prev_codepoint = 0;
    mgl_vec2f glyph_offset = {0, 0};
    unsigned int rows = 1;

    for(size_t i = 0; i < self->text_size;) {
        unsigned char *cp = (unsigned char*)&self->text[i];
        uint32_t codepoint = 0;
        size_t clen = 0;
        if(!mgl_utf8_decode(cp, self->text_size - i, &codepoint, &clen)) {
            codepoint = *cp;
            clen = 1;
        }

        if(codepoint == '\t') {
            if(mgl_font_get_glyph(self->font, ' ', &glyph) == 0) {
                kerning = mgl_font_get_kerning(self->font, prev_codepoint, codepoint);
                glyph_width = glyph.advance * TAB_WIDTH;
            } else {
                goto next;
            }
        } else if(codepoint == '\n') {
            kerning = 0.0f;
            glyph_width = 0.0f;
            glyph.size = (mgl_vec2i){ (int)glyph_width, (int)self->font->character_size };
        } else {
            if(mgl_font_get_glyph(self->font, codepoint, &glyph) == 0) {
                kerning = mgl_font_get_kerning(self->font, prev_codepoint, codepoint);
                glyph_width = glyph.advance;
            } else {
                goto next;
            }
        }

        if(((glyph_offset.x + kerning + glyph_width > self->max_width) && self->max_width > 0.001f) || codepoint == '\n') {
            glyph_offset.x = 0.0f;
            glyph_offset.y += self->font->character_size;
            prev_codepoint = 0;

            if(rows == self->max_rows)
                return;
            ++rows;
        } else {
            glyph_offset.x += kerning;
        }

        if(!callback((codepoint_loop_callback_data){ codepoint_index, codepoint, kerning, glyph_width, &glyph, glyph_offset }, userdata))
            return;

        glyph_offset.x += glyph_width;
        //if(codepoint == '\n')
        //    glyph_offset.y += self->font->character_size;

        prev_codepoint = codepoint;

        next:
        i += clen;
        ++codepoint_index;
    }
}

typedef struct {
    mgl_vec2f bounds;
} CalculateBoundsUserdata;

static bool calculate_bounds_callback(codepoint_loop_callback_data callback_data, void *userdata) {
    CalculateBoundsUserdata *calculate_bounds_userdata = userdata;
    calculate_bounds_userdata->bounds.x = max_float(calculate_bounds_userdata->bounds.x, callback_data.glyph_offset.x + callback_data.glyph_width);
    calculate_bounds_userdata->bounds.y = max_float(calculate_bounds_userdata->bounds.y, callback_data.glyph_offset.y + callback_data.glyph->size.y);
    return true;
}

static mgl_vec2f mgl_text_calculate_bounds(mgl_text *self) {
    CalculateBoundsUserdata calculate_bounds_userdata;
    calculate_bounds_userdata.bounds.x = 0.0f;
    calculate_bounds_userdata.bounds.y = self->font->character_size;
    mgl_text_for_each_codepoint(self, calculate_bounds_callback, &calculate_bounds_userdata);
    return calculate_bounds_userdata.bounds;
}

void mgl_text_init(mgl_text *self, mgl_font *font, const char *str, size_t str_size) {
    self->font = font;
    self->color = (mgl_color){ 255, 255, 255, 255 };
    self->position = (mgl_vec2f){ 0.0f, 0.0f };
    self->text = NULL;
    self->text_size = 0;
    self->bounds_dirty = true;
    self->max_width = 0.0f;
    self->max_rows = 0;
    mgl_text_set_string(self, str, str_size);
}

void mgl_text_deinit(mgl_text *self) {
    self->font = NULL;
    self->position = (mgl_vec2f){ 0.0f, 0.0f };
    self->text = NULL;
    self->text_size = 0;
    self->bounds = (mgl_vec2f){ 0.0f, 0.0f };
}

void mgl_text_set_string(mgl_text *self, const char *str, size_t str_size) {
    self->text = str;
    self->text_size = str_size;
    self->bounds_dirty = true;
}

void mgl_text_set_font(mgl_text *self, mgl_font *font) {
    self->font = font;
    self->bounds_dirty = true;
}

void mgl_text_set_position(mgl_text *self, mgl_vec2f position) {
    self->position = position;
}

void mgl_text_set_color(mgl_text *self, mgl_color color) {
    self->color = color;
}

void mgl_text_set_max_width(mgl_text *self, float max_width) {
    if(float_abs(max_width - self->max_width) < 0.01)
        return;
    self->bounds_dirty = true;
    self->max_width = max_width;
}

void mgl_text_set_max_rows(mgl_text *self, unsigned int max_rows) {
    if(max_rows != self->max_rows)
        self->bounds_dirty = true;
    self->max_rows = max_rows;
}

mgl_vec2f mgl_text_get_bounds(mgl_text *self) {
    if(self->bounds_dirty) {
        self->bounds_dirty = false;
        if(self->text && self->text_size > 0 && self->font)
            self->bounds = mgl_text_calculate_bounds(self);
        else if(self->font)
            self->bounds = (mgl_vec2f){ 0.0f, (float)self->font->character_size };
        else
            self->bounds = (mgl_vec2f){ 0.0f, 0.0f };
    }
    return self->bounds;
}

typedef struct {
    mgl_vec2f glyph_offset;
    const mgl_text *text;
    size_t index;
} FindCharacterPosUserdata;

static bool find_character_pos_callback(codepoint_loop_callback_data callback_data, void *userdata) {
    FindCharacterPosUserdata *find_character_pos_userdata = userdata;
    find_character_pos_userdata->glyph_offset = callback_data.glyph_offset;
    if(callback_data.codepoint_index >= find_character_pos_userdata->index)
        return false;

    find_character_pos_userdata->glyph_offset.x += callback_data.glyph_width;
    if(callback_data.codepoint == '\n')
        find_character_pos_userdata->glyph_offset.y += find_character_pos_userdata->text->font->character_size;
    return true;
}

mgl_vec2f mgl_text_find_character_pos(mgl_text *self, size_t index) {
    FindCharacterPosUserdata find_character_pos_userdata;
    find_character_pos_userdata.glyph_offset = (mgl_vec2f){0.0f, 0.0f};
    find_character_pos_userdata.text = self;
    find_character_pos_userdata.index = index;
    mgl_text_for_each_codepoint(self, find_character_pos_callback, &find_character_pos_userdata);
    return (mgl_vec2f){ self->position.x + find_character_pos_userdata.glyph_offset.x, self->position.y + find_character_pos_userdata.glyph_offset.y };
}

static void mgl_text_draw_glyph(mgl_context *context, const mgl_font_glyph *glyph, mgl_vec2i position) {
    context->gl.glTexCoord2f(glyph->texture_position.x, glyph->texture_position.y);
    context->gl.glVertex3f(position.x + glyph->position.x, position.y + glyph->position.y, 0.0f);

    context->gl.glTexCoord2f(glyph->texture_position.x + glyph->texture_size.x, glyph->texture_position.y);
    context->gl.glVertex3f(position.x + glyph->position.x + glyph->size.x, position.y + glyph->position.y, 0.0f);

    context->gl.glTexCoord2f(glyph->texture_position.x + glyph->texture_size.x, glyph->texture_position.y + glyph->texture_size.y);
    context->gl.glVertex3f(position.x + glyph->position.x + glyph->size.x, position.y + glyph->position.y + glyph->size.y, 0.0f);

    context->gl.glTexCoord2f(glyph->texture_position.x, glyph->texture_position.y + glyph->texture_size.y);
    context->gl.glVertex3f(position.x + glyph->position.x, position.y + glyph->position.y + glyph->size.y, 0.0f);
}

typedef struct {
    mgl_vec2f position;
    const mgl_text *text;
    mgl_context *context;
} TextDrawUserdata;

static bool text_draw_callback(codepoint_loop_callback_data callback_data, void *userdata) {
    TextDrawUserdata *text_draw_userdata = userdata;
    if(callback_data.codepoint != '\t' && callback_data.codepoint != '\n') {
        mgl_text_draw_glyph(text_draw_userdata->context, callback_data.glyph,
            (mgl_vec2i){ text_draw_userdata->position.x + callback_data.glyph_offset.x, text_draw_userdata->position.y + callback_data.glyph_offset.y });
    }
    return true;
}

/* TODO: Use opengl buffer object instead */
/* TODO: Cache texture bind to not bind texture if its already bound and do not bind texture 0 */
void mgl_text_draw(mgl_context *context, mgl_text *text) {
    if(!text->text || text->text_size == 0 || !text->font)
        return;

    /* Calculate bounds beforehand if needed, which also generates glyphs because opengl outputs an error if that is done inside glBegin/glEnd */
    /* TODO: Only run this on the new glyphs */
    mgl_text_get_bounds(text);

    TextDrawUserdata text_draw_userdata;
    text_draw_userdata.position = (mgl_vec2f){ text->position.x, text->position.y };
    text_draw_userdata.text = text;
    text_draw_userdata.context = context;

    mgl_window_set_texture_blend_func(context->current_window);

    context->gl.glColor4ub(text->color.r, text->color.g, text->color.b, text->color.a);
    mgl_texture_use(&text->font->texture);
    context->gl.glBegin(GL_QUADS);
    mgl_text_for_each_codepoint(text, text_draw_callback, &text_draw_userdata);
    context->gl.glEnd();
    mgl_texture_use(NULL);

    mgl_window_set_render_blend_func(context->current_window);
}