#include "gl_util.h"

#include <assert.h>
#include <gdk/gdk.h>
#include <gio/gio.h>
#include <gmodule.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

void
gl_util_check_error(const char *file, int line)
{
        GLenum error = glGetError();

        const char *name;
        switch (error) {
        case GL_NO_ERROR:
                return; // no error
        case GL_INVALID_ENUM:
                name = "GL_INVALID_ENUM";
                break;
        case GL_INVALID_VALUE:
                name = "GL_INVALID_VALUE";
                break;
        case GL_INVALID_OPERATION:
                name = "GL_INVALID_OPERATION";
                break;
        case GL_INVALID_FRAMEBUFFER_OPERATION:
                name = "GL_INVALID_FRAMEBUFFER_OPERATION";
                break;
        case GL_OUT_OF_MEMORY:
                name = "GL_OUT_OF_MEMORY";
                break;
        default:
                name = "UNKNOWN ERROR!";
                break;
        }

        printf("GL error at %s:%d - %s\n", file, line, name);

        // raise(SIGTRAP);
}

GLuint
gl_util_load_shader(const char *resource,
                    GLenum type,
                    const char **extra_sources,
                    size_t num_extra)
{
        GdkGLContext *context = gdk_gl_context_get_current();
        assert(context);

        GLuint shader = glCreateShader(type);
        if (shader == 0) {
                return 0;
        }

        GBytes *bytes = g_resources_lookup_data(resource, 0, NULL);
        if (!bytes) {
                printf("Failed to load shader resource %s\n", resource);
                return 0;
        }

        // Build #define for OpenGL context information
        gboolean is_es = gdk_gl_context_get_use_es(context);
        int major, minor;
        gdk_gl_context_get_version(context, &major, &minor);
        char context_info_buf[128];
        snprintf(context_info_buf,
                 128,
                 "#define %s\n#define GL_%d\n#define GL_%d_%d\n",
                 is_es ? "GL_ES" : "GL_NO_ES",
                 major,
                 major,
                 minor);

        gsize glib_size = 0;
        const GLchar *source = g_bytes_get_data(bytes, &glib_size);
        if (glib_size == 0 || glib_size > INT_MAX) {
                printf("Invalid size for resource\n");
                return 0;
        }

        const GLchar **sources = malloc((num_extra + 1) * sizeof(GLchar *));
        GLint *sizes = malloc((num_extra + 1) * sizeof(GLint));

        for (size_t i = 0; i < num_extra; ++i) {
                sources[i] = extra_sources[i];
                sizes[i] = -1;
        }
        sources[num_extra] = source;
        sizes[num_extra] = glib_size;

        glShaderSource(shader, num_extra + 1, sources, sizes);
        glCompileShader(shader);
        check_gl();

        free(sources);
        free(sizes);

        g_bytes_unref(bytes);

        // Check compile status
        GLint success;
        glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
        if (success == GL_FALSE) {
                printf("Shader compilation failed for %s\n", resource);

                glDeleteShader(shader);
                return 0;
        }

        GLint log_length;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_length);
        if (log_length > 0) {
                char *log = malloc(sizeof(char) * log_length);
                glGetShaderInfoLog(shader, log_length - 1, &log_length, log);

                printf("Shader %s log: %s\n", resource, log);
                free(log);

                glDeleteShader(shader);
                return 0;
        }

        return shader;
}

GLuint
gl_util_link_program(GLuint *shaders, size_t num_shaders)
{
        GLuint program = glCreateProgram();

        for (size_t i = 0; i < num_shaders; ++i) {
                glAttachShader(program, shaders[i]);
        }

        glLinkProgram(program);
        check_gl();

        GLint success;
        glGetProgramiv(program, GL_LINK_STATUS, &success);
        if (success == GL_FALSE) {
                printf("Program linking failed\n");
        }

        GLint log_length;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &log_length);
        if (log_length > 0) {
                char *log = malloc(sizeof(char) * log_length);
                glGetProgramInfoLog(program, log_length - 1, &log_length, log);

                printf("Program log: %s\n", log);
                free(log);
        }
        check_gl();

        return program;
}

static const GLfloat quad_data[] = {
        // Vertices
        -1,
        -1,
        1,
        -1,
        -1,
        1,
        1,
        1,
        // Texcoords
        0,
        0,
        1,
        0,
        0,
        1,
        1,
        1,
};

GLuint
gl_util_new_quad()
{
        GdkGLContext *context = gdk_gl_context_get_current();
        assert(context);

        if (gdk_gl_context_get_use_es(context)) {
                return 0;
        } else {
                GLuint buffer;
                glGenBuffers(1, &buffer);

                glBindBuffer(GL_ARRAY_BUFFER, buffer);
                glBufferData(GL_ARRAY_BUFFER,
                             sizeof(quad_data),
                             quad_data,
                             GL_STATIC_DRAW);
                check_gl();

                glBindBuffer(GL_ARRAY_BUFFER, 0);
                check_gl();

                return buffer;
        }
}

void
gl_util_bind_quad(GLuint buffer)
{
        GdkGLContext *context = gdk_gl_context_get_current();
        assert(context);

        if (gdk_gl_context_get_use_es(context)) {
                glVertexAttribPointer(
                        GL_UTIL_VERTEX_ATTRIBUTE, 2, GL_FLOAT, 0, 0, quad_data);
                check_gl();
                glEnableVertexAttribArray(GL_UTIL_VERTEX_ATTRIBUTE);
                check_gl();

                glVertexAttribPointer(GL_UTIL_TEX_COORD_ATTRIBUTE,
                                      2,
                                      GL_FLOAT,
                                      0,
                                      0,
                                      quad_data + 8);
                check_gl();
                glEnableVertexAttribArray(GL_UTIL_TEX_COORD_ATTRIBUTE);
                check_gl();
        } else {
                glBindBuffer(GL_ARRAY_BUFFER, buffer);
                check_gl();

                glVertexAttribPointer(
                        GL_UTIL_VERTEX_ATTRIBUTE, 2, GL_FLOAT, GL_FALSE, 0, 0);
                glEnableVertexAttribArray(GL_UTIL_VERTEX_ATTRIBUTE);
                check_gl();

                glVertexAttribPointer(GL_UTIL_TEX_COORD_ATTRIBUTE,
                                      2,
                                      GL_FLOAT,
                                      GL_FALSE,
                                      0,
                                      (void *)(8 * sizeof(float)));
                glEnableVertexAttribArray(GL_UTIL_TEX_COORD_ATTRIBUTE);
                check_gl();
        }
}

void
gl_util_draw_quad(GLuint buffer)
{
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        check_gl();
}
