#include "Font.h"
#include "ruby.h"
#include "GLTexture.h"
#include <iostream>
#include <stdint.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#include <unordered_map>

#define round_up_26_6(val) (((val) + 63) >> 6u)

class Glyph
{
public:
    Glyph();
    bool load(FT_Face face, FT_ULong char_code);
    int get_advance() { return m_advance; }
    void render()
    {
        m_texture->bind();
        glEnableVertexAttribArray(0);
        glBindBuffer(GL_ARRAY_BUFFER, m_vbo_id);
        glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    }
protected:
    bool m_loaded;
    int m_advance;
    GLTextureRef m_texture;
    GLuint m_vbo_id;
};
typedef Ref<Glyph> GlyphRef;

typedef struct
{
    FT_Face face;
    std::unordered_map<FT_ULong, GlyphRef> glyphs;
} Font;

static FT_Library ft;
static VALUE ruby_class;

static void Init_FreeType()
{
    static bool initialized = false;

    if (initialized)
        return;

    if (FT_Init_FreeType(&ft) != 0)
    {
        rb_raise(rb_eRuntimeError, "Error initializing FreeType");
    }

    initialized = true;
}

void Font_free(void * ptr)
{
    Font * font = (Font *)ptr;

    FT_Done_Face(font->face);

    delete font;
}

static GlyphRef get_glyph(Font * font, FT_ULong char_code)
{
    auto it = font->glyphs.find(char_code);
    if (it != font->glyphs.end())
        return it->second;
    GlyphRef glyph = new Glyph();
    glyph = glyph->load(font->face, char_code) ? glyph : NULL;
    font->glyphs[char_code] = glyph;
    return glyph;
}

static VALUE Font_new(VALUE klass, VALUE fname, VALUE size)
{
    Init_FreeType();

    Font * font = new Font();

    fname = rb_funcall(fname, rb_intern("to_s"), 0);
    const char * fname_cstr = StringValueCStr(fname);

    if (FT_New_Face(ft, fname_cstr, 0, &font->face) != 0)
    {
        delete font;
        rb_raise(rb_eRuntimeError,
                 "Could not load font %s",
                 fname_cstr);
    }

    FT_Set_Pixel_Sizes(font->face, 0, NUM2INT(size));

    static const char preload_glyphs[] = "HMgjqxy_|^";
    int max_top = -9999;
    int min_bottom = 9999;

    for (unsigned int i = 0; i < sizeof(preload_glyphs) - 1u; i++)
    {
        GlyphRef g = get_glyph(font, preload_glyphs[i]);
        if (g == NULL)
        {
            delete font;
            rb_raise(rb_eRuntimeError,
                     "Error loading glyph '%c' from font '%s'",
                     preload_glyphs[i],
                     fname_cstr);
        }
        if (font->face->glyph->bitmap_top > max_top)
            max_top = font->face->glyph->bitmap_top;
        int bitmap_bottom = font->face->glyph->bitmap_top - font->face->glyph->bitmap.rows;
        if (bitmap_bottom < min_bottom)
            min_bottom = bitmap_bottom;
    }

    int line_height = round_up_26_6(font->face->size->metrics.height);
    int baseline_offset = (line_height - (max_top - min_bottom)) / 2;

    VALUE rv = Data_Wrap_Struct(klass, NULL, Font_free, font);
    rb_iv_set(rv, "@advance", INT2FIX(get_glyph(font, 'x')->get_advance()));
    rb_iv_set(rv, "@line_height", INT2FIX(line_height));
    rb_iv_set(rv, "@baseline_offset", INT2FIX(baseline_offset));
    return rv;
}

void Font_Init()
{
    ruby_class = rb_define_class("Font", rb_cObject);
    rb_define_singleton_method(ruby_class, "new", (VALUE(*)(...))Font_new, 2);
    rb_define_attr(ruby_class, "advance", 1, 0);
    rb_define_attr(ruby_class, "line_height", 1, 0);
}

Glyph::Glyph()
{
    m_loaded = false;
}

bool Glyph::load(FT_Face face, FT_ULong char_code)
{
    if (FT_Load_Char(face, char_code, FT_LOAD_RENDER) != 0)
        return false;

    int width = face->glyph->bitmap.width;
    int height = face->glyph->bitmap.rows;
    int left = face->glyph->bitmap_left;
    int top = face->glyph->bitmap_top;
    m_advance = round_up_26_6(face->glyph->advance.x);
    uint8_t * texture = new uint8_t[width * height];
    for (int i = 0; i < height; i++)
    {
        memcpy(&texture[width * i],
               &face->glyph->bitmap.buffer[width * (height - i - 1)],
               width);
    }

    glActiveTexture(GL_TEXTURE0);
    m_texture = new GLTexture();
    m_texture->create(width, height);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_ALPHA, GL_UNSIGNED_BYTE, texture);
    delete[] texture;

    float s_max = width / (float)m_texture->get_width();
    float t_max = height / (float)m_texture->get_height();
    GLfloat box[4][4] = {
        {(GLfloat)left, (GLfloat)(top - height),
            0.0, 0.0},
        {(GLfloat)(left + width), (GLfloat)(top - height),
            s_max, 0.0},
        {(GLfloat)left, (GLfloat)top,
            0.0, t_max},
        {(GLfloat)(left + width), (GLfloat)top,
            s_max, t_max},
    };
    glGenBuffers(1, &m_vbo_id);
    glBindBuffer(GL_ARRAY_BUFFER, m_vbo_id);
    glBufferData(GL_ARRAY_BUFFER, sizeof(box), box, GL_STATIC_DRAW);

    m_loaded = true;
    return true;
}
