/*
 * COPYRIGHT: Stealthy Labs LLC
 * DATE: 29th May 2015
 * AUTHOR: Stealthy Labs
 * SOFTWARE: Tea Time
 */
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>
#include <teatime.h>

static int teatime_check_gl_version(uint32_t *major, uint32_t *minor);
static int teatime_check_program_errors(GLuint program);
static int teatime_check_shader_errors(GLuint shader);

#define TEATIME_BREAKONERROR(FN,RC)  if ((RC = teatime_check_gl_errors(__LINE__, #FN )) < 0) break
#define TEATIME_BREAKONERROR_FB(FN,RC)  if ((RC = teatime_check_gl_fb_errors(__LINE__, #FN )) < 0) break

teatime_t *teatime_setup()
{
    int rc = 0;
    teatime_t *obj = calloc(1, sizeof(teatime_t));
    if (!obj) {
        fprintf(stderr, "Out of memory allocating %zu bytes\n",
                sizeof(teatime_t));
        return NULL;
    }
    do {
        uint32_t version[2] = { 0, 0};
        if (teatime_check_gl_version(&version[0], &version[1]) < 0) {
            fprintf(stderr, "Unable to verify OpenGL version\n");
            rc = -1;
            break;
        }
        if (version[0] < 3) {
            fprintf(stderr, "Minimum Required OpenGL version 3.0. You have %u.%u\n",
                    version[0], version[1]);
            rc = -1;
            break;
        }
        /* initialize off-screen framebuffer */
        /*
         * This is the EXT_framebuffer_object OpenGL extension that allows us to
         * use an offscreen buffer as a target for rendering operations such as
         * vector calculations, providing full precision and removing unwanted
         * clamping issues.
         * we are turning off the traditional framebuffer here apparently.
         */
        glGenFramebuffersEXT(1, &(obj->ofb));
        glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, obj->ofb);
        TEATIME_BREAKONERROR(glBindFramebufferEXT, rc);
        fprintf(stderr, "Successfully created off-screen framebuffer with id: %d\n",
                obj->ofb);
        /* get the texture size */
        obj->maxtexsz = -1;
        glGetIntegerv(GL_MAX_TEXTURE_SIZE, &(obj->maxtexsz));
        fprintf(stderr, "Maximum Texture size for the GPU: %d\n", obj->maxtexsz);
        obj->itexid = obj->otexid = 0;
        obj->shader = obj->program = 0;
    } while (0);
    if (rc < 0) {
        teatime_cleanup(obj);
        obj = NULL;
    }
    return obj;
}

void teatime_cleanup(teatime_t *obj)
{
    if (obj) {
        teatime_delete_program(obj);
        teatime_delete_textures(obj);
        glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
        glDeleteFramebuffersEXT(1, &(obj->ofb));
        glFlush();
        free(obj);
        obj = NULL;
    }
}

int teatime_set_viewport(teatime_t *obj, uint32_t ilen)
{
    uint32_t texsz = (uint32_t)((long)(sqrt(ilen / 4.0)));
    if (obj && texsz > 0 && texsz < (GLuint)obj->maxtexsz) {
        /* viewport mapping 1:1 pixel = texel = data mapping */
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        gluOrtho2D(0.0, texsz, 0.0, texsz);
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        glViewport(0, 0, texsz, texsz);
        obj->tex_size = texsz;
        fprintf(stderr, "Texture size: %u x %u\n", texsz, texsz);
        return 0;
    } else if (obj) {
        fprintf(stderr, "Max. texture size is %d. Calculated: %u from input length: %u\n",
                obj->maxtexsz, texsz, ilen);
    }
    return -EINVAL;
}


int teatime_create_textures(teatime_t *obj, const uint32_t *input, uint32_t ilen)
{
    if (obj && input && ilen > 0) {
        int rc = 0;
        do {
            uint32_t texsz = (uint32_t)((long)(sqrt(ilen / 4.0)));
            if (texsz != obj->tex_size) {
                fprintf(stderr, "Viewport texture size(%u) != Input texture size (%u)\n",
                        obj->tex_size, texsz);
                rc = -EINVAL;
                break;
            }
            glGenTextures(1, &(obj->itexid));
            glGenTextures(1, &(obj->otexid));
            fprintf(stderr, "Created input texture with ID: %u\n", obj->itexid);
            fprintf(stderr, "Created output texture with ID: %u\n", obj->otexid);
            /** BIND ONE TEXTURE AT A TIME **/
            /* the texture target can vary depending on GPU */
            glBindTexture(GL_TEXTURE_2D, obj->itexid);
            TEATIME_BREAKONERROR(glBindTexture, rc);
            glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
            /* turn off filtering and set proper wrap mode - this is obligatory for
             * floating point textures */
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            TEATIME_BREAKONERROR(glTexParameteri, rc);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            TEATIME_BREAKONERROR(glTexParameteri, rc);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
            TEATIME_BREAKONERROR(glTexParameteri, rc);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
            TEATIME_BREAKONERROR(glTexParameteri, rc);
            /* create a 2D texture of the same size as the data
             * internal format: GL_RGBA32UI_EXT
             * texture format: GL_RGBA_INTEGER
             * texture type: GL_UNSIGNED_INT
             */
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32UI_EXT,
                    texsz, texsz, 0, GL_RGBA_INTEGER, GL_UNSIGNED_INT, NULL);
            TEATIME_BREAKONERROR(glTexImage2D, rc);
            /* transfer data to texture */
#ifdef WIN32
            glTexSubImage2D
#else
            glTexSubImage2DEXT
#endif
                (GL_TEXTURE_2D, 0, 0, 0, obj->tex_size,
                    obj->tex_size, GL_RGBA_INTEGER, GL_UNSIGNED_INT, input);
#ifdef WIN32
            TEATIME_BREAKONERROR(glTexSubImage2D, rc);
#else
            TEATIME_BREAKONERROR(glTexSubImage2DEXT, rc);
#endif
            fprintf(stderr, "Successfully transferred input data to texture ID: %u\n", obj->itexid);
            /* BIND the OUTPUT texture and work on it */
            /* the texture target can vary depending on GPU */
            glBindTexture(GL_TEXTURE_2D, obj->otexid);
            TEATIME_BREAKONERROR(glBindTexture, rc);
            /* turn off filtering and set proper wrap mode - this is obligatory for
             * floating point textures */
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            TEATIME_BREAKONERROR(glTexParameteri, rc);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            TEATIME_BREAKONERROR(glTexParameteri, rc);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
            TEATIME_BREAKONERROR(glTexParameteri, rc);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
            TEATIME_BREAKONERROR(glTexParameteri, rc);
            /* create a 2D texture of the same size as the data
             * internal format: GL_RGBA32UI_EXT
             * texture format: GL_RGBA_INTEGER
             * texture type: GL_UNSIGNED_INT
             */
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32UI_EXT,
                    texsz, texsz, 0, GL_RGBA_INTEGER, GL_UNSIGNED_INT, NULL);
            TEATIME_BREAKONERROR(glTexImage2D, rc);
            /* change tex-env to replace instead of the default modulate */
            glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
            TEATIME_BREAKONERROR(glTexEnvi, rc);
            /* attach texture */
            glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT,
                    GL_TEXTURE_2D, obj->otexid, 0);
            TEATIME_BREAKONERROR(glFramebufferTexture2DEXT, rc);
            TEATIME_BREAKONERROR_FB(glFramebufferTexture2DEXT, rc);
            glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT);
            TEATIME_BREAKONERROR(glDrawBuffer, rc);
            glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT1_EXT,
                    GL_TEXTURE_2D, obj->otexid, 0);
            TEATIME_BREAKONERROR(glFramebufferTexture2DEXT, rc);
