#include "GLProgram.h"
#include "GLShader.h"
#include <string.h>

static VALUE ruby_class;

static void GLProgram_free(void * ptr)
{
    GLProgram * program = (GLProgram *)ptr;

    if (program->id > 0u)
    {
        glDeleteProgram(program->id);
    }

    delete program;
}

static VALUE GLProgram_new(int argc, VALUE * argv, VALUE klass)
{
    GLuint id = glCreateProgram();
    if (id == 0u)
    {
        rb_raise(rb_eRuntimeError, "Error allocating GL program object");
    }

    GLProgram * glprogram = new GLProgram();
    glprogram->id = id;

    VALUE rv = Data_Wrap_Struct(klass, NULL, GLProgram_free, glprogram);
    rb_obj_call_init(rv, argc, argv);

    return rv;
}

static VALUE GLProgram_attach_shader(VALUE self, VALUE gl_shader)
{
    GLProgram * glprogram;
    Data_Get_Struct(self, GLProgram, glprogram);
    glAttachShader(glprogram->id, GLShader_id(gl_shader));

    return Qnil;
}

static VALUE GLProgram_bind_attribute(VALUE self, VALUE attr_name, VALUE index)
{
    GLProgram * glprogram;
    Data_Get_Struct(self, GLProgram, glprogram);
    attr_name = rb_funcall(attr_name, rb_intern("to_s"), 0);
    const char * attr_name_cstr = StringValueCStr(attr_name);
    glBindAttribLocation(glprogram->id, NUM2UINT(index), attr_name_cstr);

    return Qnil;
}

static VALUE GLProgram_link(VALUE self)
{
    GLProgram * glprogram;
    Data_Get_Struct(self, GLProgram, glprogram);

    glLinkProgram(glprogram->id);

    GLint link_status;
    glGetProgramiv(glprogram->id, GL_LINK_STATUS, &link_status);
    if (link_status != GL_TRUE)
    {
        GLint log_length;
        glGetProgramiv(glprogram->id, GL_INFO_LOG_LENGTH, &log_length);
        if (log_length > 0)
        {
            char log[log_length + 1];
            glGetProgramInfoLog(glprogram->id, log_length, &log_length, log);
            log[log_length] = '\0';
            glDeleteProgram(glprogram->id);
            rb_raise(rb_eRuntimeError,
                     "Error linking program:\n%s\n%s\n%s\n",
                     "--------------------",
                     log,
                     "--------------------");
        }
        else
        {
            glDeleteProgram(glprogram->id);
            rb_raise(rb_eRuntimeError, "Error linking program");
        }
    }

    return Qnil;
}

static VALUE GLProgram_load_uniform_location(VALUE self, VALUE uniform)
{
    static const char * uniform_names[] = {
        "viewport_size",
        "position",
        "size",
        "texture",
        "color",
    };
    GLProgram * glprogram;
    Data_Get_Struct(self, GLProgram, glprogram);
    uniform = rb_funcall(uniform, rb_intern("to_s"), 0);
    const char * uniform_cstr = StringValueCStr(uniform);
    int uniform_index = -1;
    for (int i = 0;
         i < (int)(sizeof(uniform_names) / sizeof(uniform_names[0]));
         i++)
    {
        if (strcmp(uniform_cstr, uniform_names[i]) == 0)
        {
            uniform_index = i;
            break;
        }
    }
    if (uniform_index >= 0)
    {
        GLint location = glGetUniformLocation(glprogram->id, uniform_cstr);
        if (location < 0)
        {
            rb_raise(rb_eRuntimeError,
                     "Cannot determine uniform location for '%s'",
                     uniform_cstr);
        }
        glprogram->uniforms[uniform_index] = location;
    }
    else
    {
        rb_raise(rb_eRuntimeError,
                 "Unknown uniform name '%s'",
                 uniform_cstr);
    }
    return Qnil;
}

void GLProgram_Init()
{
    ruby_class = rb_define_class("GLProgram", rb_cObject);
    rb_define_singleton_method(ruby_class, "new", (VALUE(*)(...))GLProgram_new, -1);
    rb_define_method(ruby_class, "attach_shader", (VALUE(*)(...))GLProgram_attach_shader, 1);
    rb_define_method(ruby_class, "bind_attribute", (VALUE(*)(...))GLProgram_bind_attribute, 2);
    rb_define_method(ruby_class, "link", (VALUE(*)(...))GLProgram_link, 0);
    rb_define_method(ruby_class, "load_uniform_location", (VALUE(*)(...))GLProgram_load_uniform_location, 1);
}
