#include "os.h"
#include "ph.h"
#include "sphere.c"

#define BUFFER_WIDTH  128
#define BUFFER_HEIGHT 128
#define VIEW_WIDTH    (1 + BUFFER_WIDTH + 1 + BUFFER_WIDTH + 1 + BUFFER_WIDTH + 1 + BUFFER_WIDTH + 1 + BUFFER_WIDTH + 1)
#define VIEW_HEIGHT   (1 + BUFFER_HEIGHT + 1 + (BUFFER_HEIGHT / 2) + 1 + (BUFFER_HEIGHT / 4) + 1 + (BUFFER_HEIGHT / 8) + 1)
#define FILTER_COUNT  4
#define KERNEL_SIZE   3
#define STRINGIFY(A)  #A

#include "shaders/passv.glsl"
#include "shaders/blitf.glsl"
#include "shaders/combine5f.glsl"
#include "shaders/row3f.glsl"
#include "shaders/lightv.glsl"
#include "shaders/lightf.glsl"
#include "shaders/hipassf.glsl"

PHsurface window;
PHsurface scene;
PHsurface pass0[FILTER_COUNT];
PHsurface pass1[FILTER_COUNT];
GLuint sphereVerts;
GLuint sphereFaces;
GLuint blitProg;
GLuint combineProg;
GLuint filterProg;
GLuint lightProg;
GLuint hiPassProg;

GLfloat darkblue[4] = {7, 16, 141, 0};
GLfloat lightblue[4] = {122, 143, 248, 0};
GLfloat brightgold[4] = {5*0.8f, 5*0.498039f, 5*0.196078f, 1.0f};
GLfloat black[4] = {0, 0, 0, 1};
GLfloat white[4] = {1, 1, 1, 1};
GLfloat gray[4] = {0.5f, 0.5f, 0.5f, 1};

float kernel[KERNEL_SIZE] = { 5, 6, 5 };

typedef enum {HORIZONTAL, VERTICAL} Direction;

static void blur(PHsurface *sources, PHsurface *dests, int count, Direction dir)
{
    GLint loc;
    int p;

    // Set up the filter.
    glUseProgram(filterProg);
    loc = glGetUniformLocation(filterProg, "source");
    glUniform1i(loc, 0);
    loc = glGetUniformLocation(filterProg, "coefficients");
    glUniform1fv(loc, KERNEL_SIZE, kernel);
    loc = glGetUniformLocation(filterProg, "offsetx");
    glUniform1f(loc, 0);
    loc = glGetUniformLocation(filterProg, "offsety");
    glUniform1f(loc, 0);
    if (dir == HORIZONTAL)
        loc = glGetUniformLocation(filterProg, "offsetx");

    // Perform the blurring.
    for (p = 0; p < count; p++)
    {
        float offset = 1.2f / sources[p].width;
        glUniform1f(loc, offset);
        phBindSurface(dests + p);
        glBindTexture(GL_TEXTURE_2D, sources[p].texture);
        glBegin(GL_QUADS);
        glTexCoord2i(0, 0); glVertex2i(-1, -1);
        glTexCoord2i(1, 0); glVertex2i(1, -1);
        glTexCoord2i(1, 1); glVertex2i(1, 1);
        glTexCoord2i(0, 1); glVertex2i(-1, 1);
        glEnd();
    }
}

static void init()
{
    int p, c;
    float sum;
    GLsizei width;
    GLsizei height;

    if (!GLEW_VERSION_2_0)
        fatalf("OpenGL 2.0 is required");

    if (!glewIsSupported("GL_ARB_half_float_pixel"))
        fatalf("GL_ARB_half_float_pixel is required");

    if (!glewIsSupported("GL_EXT_framebuffer_object"))
        fatalf("GL_EXT_framebuffer_object is required");

    glGenBuffers(1, &sphereVerts);
    glBindBuffer(GL_ARRAY_BUFFER, sphereVerts);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertData), (void *) vertData, GL_STATIC_DRAW);

    glGenBuffers(1, &sphereFaces);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, sphereFaces);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(faceData), (void *) faceData, GL_STATIC_DRAW);

    glMatrixMode(GL_MODELVIEW);

    // Compile shaders
    blitProg = phCompile(passv, blitf);
    combineProg = phCompile(passv, combine5f);
    filterProg = phCompile(passv, row3f);
    lightProg = phCompile(lightv, lightf);
    hiPassProg = phCompile(passv, hipassf);

    // Normalize kernel coefficients
    sum = 0;
    for (c = 0; c < KERNEL_SIZE; c++)
        sum += kernel[c];
    for (c = 0; c < KERNEL_SIZE; c++)
        kernel[c] /= sum;

    // Normalize colors
    for (c = 0; c < 4; c++)
    {
        darkblue[c] /= 255.0f;
        lightblue[c] /= 255.0f;
    }

    // Create Window Surface
    window.fbo = 0;
    window.depth = 0;
    window.width = VIEW_WIDTH;
    window.height = VIEW_HEIGHT;
    window.clearColor[0] = lightblue[0];
    window.clearColor[1] = lightblue[1];
    window.clearColor[2] = lightblue[2];
    window.clearColor[3] = lightblue[3];
    window.viewport.x = 0;
    window.viewport.y = 0;
    window.viewport.width = window.width;
    window.viewport.height = window.height;
    glLoadIdentity();
    glGetFloatv(GL_MODELVIEW_MATRIX, window.modelview);
    glOrtho(0, window.width, window.height, 0, 0, 10);
    glGetFloatv(GL_MODELVIEW_MATRIX, window.projection);
    glLoadIdentity();

    // Create 3D Scene Surface
    width = BUFFER_WIDTH;
    height = BUFFER_HEIGHT;
    scene.width = width;
    scene.height = height;
    scene.clearColor[0] = 0;
    scene.clearColor[1] = 0;
    scene.clearColor[2] = 0;
    scene.clearColor[3] = 0;
    scene.viewport.x = 0;
    scene.viewport.y = 0;
    scene.viewport.width = width;
    scene.viewport.height = height;
    glGetFloatv(GL_MODELVIEW_MATRIX, scene.modelview);
    glGetFloatv(GL_MODELVIEW_MATRIX, scene.projection);
    phCreateSurface(&scene, GL_TRUE, GL_TRUE, GL_FALSE);

    // Create Pass Surfaces
    for (p = 0; p < FILTER_COUNT; p++)
    {
        pass0[p].width = width;
        pass0[p].height = height;
        pass0[p].viewport.x = 0;
        pass0[p].viewport.y = 0;
        pass0[p].viewport.width = width;
        pass0[p].viewport.height = height;
        glGetFloatv(GL_MODELVIEW_MATRIX, pass0[p].modelview);
        glGetFloatv(GL_MODELVIEW_MATRIX, pass0[p].projection);
        phCreateSurface(pass0 + p, GL_FALSE, GL_FALSE, GL_TRUE);
        width = width >> 1;
        height = height >> 1;
    }
    width = BUFFER_WIDTH;
    height = BUFFER_HEIGHT;
    for (p = 0; p < FILTER_COUNT; p++)
    {
        pass1[p].width = width;
        pass1[p].height = height;
        pass1[p].viewport.x = 0;
        pass1[p].viewport.y = 0;
        pass1[p].viewport.width = width;
        pass1[p].viewport.height = height;
        glGetFloatv(GL_MODELVIEW_MATRIX, pass1[p].modelview);
        glGetFloatv(GL_MODELVIEW_MATRIX, pass1[p].projection);
        phCreateSurface(pass1 + p, GL_FALSE, GL_FALSE, GL_FALSE);
        width = width >> 1;
        height = height >> 1;
    }
}

static void computeLightingParams(float theta, PHvec3 *vp, PHvec3 *hhat)
{
    PHvec3 position;
    PHvec3 look = {0, 0, 0};
    PHvec3 eye  = {0, 0, 1};

    position.x = 0;
    position.y = sinf(theta * PH_PI / 180.0f);
    position.z = cosf(theta * PH_PI / 180.0f);

    *vp = phSub(&look, &position);
    *hhat = phAdd(vp, &eye);
    phNormalize(hhat);
}

static void draw()
{
    int p;
    static float theta = 50;
    GLint loc;
    PHvec3 vp;
    PHvec3 hhat;

    // Draw 3D scene.
    srand(0);
    phBindSurface(&scene);
    phClearSurface();
    glTranslatef(0, 0, -1);
    glUseProgram(lightProg);
    computeLightingParams(theta, &vp, &hhat);

    glEnable(GL_DEPTH_TEST);
    loc = glGetUniformLocation(lightProg, "shininess");
    glUniform1f(loc, 64);
    loc = glGetUniformLocation(lightProg, "diffuse");
    glUniform3fv(loc, 1, gray);
    loc = glGetUniformLocation(lightProg, "specular");
    glUniform3fv(loc, 1, brightgold);
    loc = glGetUniformLocation(lightProg, "ambient");
    glUniform3fv(loc, 1, darkblue);
    loc = glGetUniformLocation(lightProg, "vp");
    glUniform3fv(loc, 1, &vp.x);
    loc = glGetUniformLocation(lightProg, "hhat");
    glUniform3fv(loc, 1, &hhat.x);

    glScalef(0.5f, 0.5f, 0.5f);
    glBindBuffer(GL_ARRAY_BUFFER, sphereVerts);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, sphereFaces);
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_NORMAL_ARRAY);
    glVertexPointer(3, GL_FLOAT, 0, 0);
    glNormalPointer(GL_FLOAT, 0, 0);
    glPushMatrix();
    glDrawElements(GL_TRIANGLES, FACE_COUNT * 3, GL_UNSIGNED_INT, 0);
    glPopMatrix();
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);
    glDisable(GL_DEPTH_TEST);

    // Hi-pass filter into pass0[0]
    glUseProgram(hiPassProg);
    loc = glGetUniformLocation(hiPassProg, "source");
    glUniform1i(loc, 0);
    glEnable(GL_TEXTURE_2D);
    glBindTexture(GL_TEXTURE_2D, scene.texture);
    phBindSurface(pass0);
    glBegin(GL_QUADS);
    glTexCoord2i(0, 0); glVertex2i(-1, -1);
    glTexCoord2i(1, 0); glVertex2i(1, -1);
    glTexCoord2i(1, 1); glVertex2i(1, 1);
    glTexCoord2i(0, 1); glVertex2i(-1, 1);
    glEnd();
    glUseProgram(0);

    // Downsample the scene into the source surfaces.
    glBindTexture(GL_TEXTURE_2D, pass0[0].texture);
    for (p = 1; p < FILTER_COUNT; p++)
    {
        phBindSurface(pass0 + p);
        glBegin(GL_QUADS);
        glTexCoord2i(0, 0); glVertex2i(-1, -1);
        glTexCoord2i(1, 0); glVertex2i(1, -1);
        glTexCoord2i(1, 1); glVertex2i(1, 1);
        glTexCoord2i(0, 1); glVertex2i(-1, 1);
        glEnd();
    }

    // Perform the horizontal blurring pass.
    blur(pass0, pass1, FILTER_COUNT, HORIZONTAL);

    // Draw left portion of window.
    glUseProgram(blitProg);
    loc = glGetUniformLocation(blitProg, "bkgd");
    glUniform4fv(loc, 1, black);
    loc = glGetUniformLocation(blitProg, "source");
    glUniform1i(loc, 0);
    phBindSurface(&window);
    phClearSurface();
    glTranslatef(1, 1, 0);
    glBindTexture(GL_TEXTURE_2D, scene.texture);
    glBegin(GL_QUADS);
    glTexCoord2i(0, 0); glVertex2i(0, 0);
    glTexCoord2i(1, 0); glVertex2i(scene.width, 0);
    glTexCoord2i(1, 1); glVertex2i(scene.width, scene.height);
    glTexCoord2i(0, 1); glVertex2i(0, scene.height);
    glEnd();
    glTranslatef((GLfloat) scene.width + 1, 0, 0);

    glPushMatrix();
    for (p = 0; p < FILTER_COUNT; p++)
    {
        glBindTexture(GL_TEXTURE_2D, pass0[p].texture);
        glBegin(GL_QUADS);
        glTexCoord2i(0, 0); glVertex2i(0, 0);
        glTexCoord2i(1, 0); glVertex2i(pass0[p].width, 0);
        glTexCoord2i(1, 1); glVertex2i(pass0[p].width, pass0[p].height);
        glTexCoord2i(0, 1); glVertex2i(0, pass0[p].height);
        glEnd();
        glTranslatef(0, (GLfloat) pass0[p].height + 1, 0);
    }
    glPopMatrix();
    glTranslatef((GLfloat) pass0[0].width + 1, 0, 0);
    for (p = 0; p < FILTER_COUNT; p++)
    {
        glBindTexture(GL_TEXTURE_2D, pass1[p].texture);
        glBegin(GL_QUADS);
        glTexCoord2i(0, 0); glVertex2i(0, 0);
        glTexCoord2i(1, 0); glVertex2i(pass1[p].width, 0);
        glTexCoord2i(1, 1); glVertex2i(pass1[p].width, pass1[p].height);
        glTexCoord2i(0, 1); glVertex2i(0, pass1[p].height);
        glEnd();
        glTranslatef(0, (GLfloat) pass1[p].height + 1, 0);
    }

    // Perform the vertical blurring pass.
    blur(pass1, pass0, FILTER_COUNT, VERTICAL);

    // Draw right portion of window.
    phBindSurface(&window);
    glTranslatef((GLfloat) 3 * pass1[0].width + 4, 1, 0);
    glUseProgram(blitProg);

    glPushMatrix();
    for (p = 0; p < FILTER_COUNT; p++)
    {
        glBindTexture(GL_TEXTURE_2D, pass0[p].texture);
        glBegin(GL_QUADS);
        glTexCoord2i(0, 0); glVertex2i(0, 0);
        glTexCoord2i(1, 0); glVertex2i(pass0[p].width, 0);
        glTexCoord2i(1, 1); glVertex2i(pass0[p].width, pass0[p].height);
        glTexCoord2i(0, 1); glVertex2i(0, pass0[p].height);
        glEnd();
        glTranslatef(0, (GLfloat) pass0[p].height + 1, 0);
    }
    glPopMatrix();
    glTranslatef((GLfloat) pass0[0].width + 1, 0, 0);

    glUseProgram(combineProg);

    for (p = 0; p < FILTER_COUNT; p++)
    {
        char name[] = "Pass#";

        glActiveTexture(GL_TEXTURE0 + p);
        glBindTexture(GL_TEXTURE_2D, pass0[p].texture);
        glEnable(GL_TEXTURE_2D);

        sprintf(name, "Pass%d", p);
        loc = glGetUniformLocation(combineProg, name);
        glUniform1i(loc, p);
    }

    glActiveTexture(GL_TEXTURE0 + FILTER_COUNT);
    glBindTexture(GL_TEXTURE_2D, scene.texture);
    glEnable(GL_TEXTURE_2D);
    loc = glGetUniformLocation(combineProg, "Scene");
    glUniform1i(loc, FILTER_COUNT);

    glBegin(GL_QUADS);
    glTexCoord2i(0, 0); glVertex2i(0, 0);
    glTexCoord2i(1, 0); glVertex2i(pass0[0].width, 0);
    glTexCoord2i(1, 1); glVertex2i(pass0[0].width, pass0[0].height);
    glTexCoord2i(0, 1); glVertex2i(0, pass0[0].height);
    glEnd();

    glUseProgram(0);
    for (p = 0; p < FILTER_COUNT; p++)
    {
        glActiveTexture(GL_TEXTURE0 + p);
        glDisable(GL_TEXTURE_2D);
    }
    glActiveTexture(GL_TEXTURE0 + FILTER_COUNT);
    glDisable(GL_TEXTURE_2D);
    glActiveTexture(GL_TEXTURE0);
}

int main(int argc, char** argv)
{
    int done = 0;
    OS_Event event;

    osInit("HDR Bloom" , VIEW_WIDTH, VIEW_HEIGHT, OS_OVERLAY, 0);
    osWaitVsync(0);
    init();

    while (!done)
    {
        while (osPollEvent(&event))
        {
            switch(event.type)
            {
                case OS_PAINT:
                    draw();
                    osSwapBuffers();
                    break;

                case OS_KEYUP:
                    switch (event.key.key)
                    {
                        case OSK_ESCAPE:
                        case 'X': case 'x':
                        case 'Q': case 'q':
                            done = 1;
                            break;
                    }
                    break;

                case OS_QUIT:
                    done = 1;
                    break;
            }
        }
        draw();
        osSwapBuffers();
    }

    osQuit();
    return 0;
}
