#include "shader.h"

static const char *vertex_shader = "\n\
#version 150 \n\
in vec4 aPosition;\n\
void main(void) {\n\
gl_Position = aPosition;\n\
}\n\
";

static GBytes *master_shader_f = NULL;
static const gchar *master_shader_code;
static gsize master_shader_code_size = 0;

static GLuint create_shader(const char *source, GLenum type)
{
    // Create the shader object
    GLuint shader = glCreateShader(type);
    // Load the shader source
    glShaderSource(shader, 1, &source, 0);
    // Compile the shader
    glCompileShader(shader);
    // Check for errors
    GLint status = 0;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
    if (status == GL_FALSE) {
        GLchar messages[1024];
        glGetShaderInfoLog(shader, sizeof(messages), 0, &messages[0]);
        g_warning("GLSL Shader Error: %s", messages);
    }
    return shader;
}

static GLuint create_program(const char *vsh, const char *fsh)
{
    // Build shaders
    GLuint vertex_shader = create_shader(vsh, GL_VERTEX_SHADER);
    GLuint fragment_shader = create_shader(fsh, GL_FRAGMENT_SHADER);
    
    // Create program
    GLuint program = glCreateProgram();
    
    // Attach shaders
    glAttachShader(program, vertex_shader);
    glAttachShader(program, fragment_shader);
    
    // Link program
    glLinkProgram(program);
    // Check for errors
    GLint status;
    glGetProgramiv(program, GL_LINK_STATUS, &status);
    
    if (status == GL_FALSE) {
        GLchar messages[1024];
        glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]);
        g_warning("GLSL Program Error: %s", messages);
    }
    
    // Delete shaders
    glDeleteShader(vertex_shader);
    glDeleteShader(fragment_shader);
    
    return program;
}

bool init_shader_with_name(shader_t *shader, const char *name)
{
    if (epoxy_gl_version() < 32) {
        return false;
    }
    
    GError *error = NULL;
    static char final_shader_code[0x10801] = {0,};
    static signed long filter_token_location = 0;
    
    if (!master_shader_code_size) {
        master_shader_f = g_resources_lookup_data(RESOURCE_PREFIX "Shaders/MasterShader.fsh", G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
        master_shader_code = g_bytes_get_data(master_shader_f, &master_shader_code_size);

        if (!master_shader_f) {
            g_warning("Failed to load master shader: %s", error->message);
            g_error_free(error);
            return false;
        }

        filter_token_location = strstr(master_shader_code, "{filter}") - master_shader_code;

        if (filter_token_location < 0) {
            g_error_free(error);
            return false;
        }
    }
    
    char shader_path[1024];
    g_snprintf(shader_path, sizeof(shader_path), RESOURCE_PREFIX "Shaders/%s.fsh", name);
    
    GBytes *shader_f = g_resources_lookup_data(shader_path, G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
    if (!shader_f) {
        g_warning("Failed to load shader \"%s\": %s", shader_path, error->message);
        g_error_free(error);
        return false;
    }

    gsize shader_code_size;
    const gchar *shader_code = g_bytes_get_data(shader_f, &shader_code_size);
    
    memset(final_shader_code, 0, sizeof(final_shader_code));
    memcpy(final_shader_code, master_shader_code, filter_token_location);
    strcpy(final_shader_code + filter_token_location, shader_code);
    strcat(final_shader_code + filter_token_location,
           master_shader_code + filter_token_location + sizeof("{filter}") - 1);

    g_bytes_unref(shader_f);

    shader->program = create_program(vertex_shader, final_shader_code);
    
    // Attributes
    shader->position_attribute = glGetAttribLocation(shader->program, "aPosition");
    // Uniforms
    shader->resolution_uniform = glGetUniformLocation(shader->program, "output_resolution");
    shader->origin_uniform = glGetUniformLocation(shader->program, "origin");

    glGenTextures(1, &shader->texture);
    glBindTexture(GL_TEXTURE_2D, shader->texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    glBindTexture(GL_TEXTURE_2D, 0);
    shader->texture_uniform = glGetUniformLocation(shader->program, "image");
    
    glGenTextures(1, &shader->previous_texture);
    glBindTexture(GL_TEXTURE_2D, shader->previous_texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    glBindTexture(GL_TEXTURE_2D, 0);
    shader->previous_texture_uniform = glGetUniformLocation(shader->program, "previous_image");
    
    shader->blending_mode_uniform = glGetUniformLocation(shader->program, "frame_blending_mode");
    
    // Program
    
    glUseProgram(shader->program);
    
    GLuint vao;
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);
    
    GLuint vbo;
    glGenBuffers(1, &vbo);
    
    // Attributes
    
    static GLfloat const quad[16] = {
        -1.f, -1.f, 0, 1,
        -1.f, +1.f, 0, 1,
        +1.f, -1.f, 0, 1,
        +1.f, +1.f, 0, 1,
    };
    
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
    glEnableVertexAttribArray(shader->position_attribute);
    glVertexAttribPointer(shader->position_attribute, 4, GL_FLOAT, GL_FALSE, 0, 0);
    
    return true;
}

void render_bitmap_with_shader(shader_t *shader, void *bitmap, void *previous,
                               unsigned source_width, unsigned source_height,
                               unsigned x, unsigned y, unsigned w, unsigned h,
                               GB_frame_blending_mode_t blending_mode)
{
    glUseProgram(shader->program);
    glUniform2f(shader->origin_uniform, x, y);
    glUniform2f(shader->resolution_uniform, w, h);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, shader->texture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, source_width, source_height, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, bitmap);
    glUniform1i(shader->texture_uniform, 0);
    glUniform1i(shader->blending_mode_uniform, previous? blending_mode : GB_FRAME_BLENDING_MODE_DISABLED);
    if (previous) {
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, shader->previous_texture);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, source_width, source_height, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, previous);
        glUniform1i(shader->previous_texture_uniform, 1);
    }
    glBindFragDataLocation(shader->program, 0, "frag_color");
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}

void free_shader(shader_t *shader)
{
    if (epoxy_gl_version() < 32) {
        return;
    }
    
    glDeleteProgram(shader->program);
    glDeleteTextures(1, &shader->texture);
    glDeleteTextures(1, &shader->previous_texture);
}

void free_master_shader(void) {
    g_bytes_unref(master_shader_f);
    master_shader_code_size = 0;
}