/*
 * (C) Copyright Ian D. Romanick 2007
 * All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * on the rights to use, copy, modify, merge, publish, distribute, sub
 * license, and/or sell copies of the Software, and to permit persons to whom
 * the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the next
 * paragraph) shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.  IN NO EVENT SHALL
 * AUTHORS, COPYRIGHT HOLDERS, AND/OR THEIR SUPPLIERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
 * USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# define _USE_MATH_DEFINES
# include <windows.h>

# pragma comment(lib, "opengl32.lib")
# pragma comment(lib, "glu32.lib")
# pragma comment(lib, "glew32.lib")
# pragma comment(lib, "zdll.lib")
# pragma comment(lib, "SDL.lib")
# pragma comment(lib, "SDLmain.lib")
# pragma comment(lib, "SDL_ttf.lib")
#endif

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <GL/glew.h>
#include <GL/glu.h>

#include <SDL/SDL.h>
#include <SDL/SDL_ttf.h>

#include "glsl_glue.h"
#include "primitives.h"
#include "vector.h"
#include "shader.h"
#include "gl_error.h"

#include "shadow.h"

static TTF_Font *font_vera = NULL;
static SDL_Surface *my_surf = NULL;
static unsigned width = 800;
static unsigned height = 600;
static unsigned win_width;
static unsigned win_height;

static int shift_status = 0;
static GLfloat fps = 0.0;
static bool done = false;
static bool anim = false;
static bool light_anim = false;
static bool is_full = false;

static GLint t0 = 0;
static GLint frames = 0;
static GLfloat x_angle = 0.0f, y_angle = 0.0f;
static GLfloat light_angle = 0.00f;

static idr_vec4 light;

GLfloat amb_color[3] = {0.02f, 0.02f, 0.05f};

static const idr_vec4 light_move[4] = {
#if 0
    idr_vec4(0.0, 0.0, 0.0, 0.0),
    idr_vec4(0.0, 0.0, 0.0, 4.0),
    idr_vec4(2.0, 0.0, 0.0, 0.0),
    idr_vec4(0.0, 0.0, 0.0, 1.0),
#else
    idr_vec4(2.0, 0.0, 0.0, 0.0),
    idr_vec4(0.0, 0.0, 0.0, 8.0),
    idr_vec4(0.0, 0.0, 0.0, 0.0),
    idr_vec4(0.0, 0.0, 0.0, 1.0),
#endif
};

#define NUM_EXTRA_OBJS 4
struct entity entities[NUM_EXTRA_OBJS + 1];

static void Redisplay(void)
{
    unsigned i;
    
    static const GLfloat extra_objs[NUM_EXTRA_OBJS][3] = {
        {  1.2f, -2.5f, -0.4f },
        { -1.2f, -2.5f, -0.4f },
        {  0.0f, -2.5f,  1.0f },
        {  0.0f,  0.0f,  0.0f }
    };

    static const GLubyte extra_colors[NUM_EXTRA_OBJS][4] = {
        { 0x00, 0x33, 0xCC, 0xFF },
        { 0x00, 0xCC, 0x33, 0xFF },
        { 0xCC, 0x33, 0x00, 0xFF },
        { 0xCC, 0x00, 0x00, 0x00 }
    };



    /* Calculate a reasonable local tranformation for the object.  Save a
     * copy of this matrix for reuse later.
     */
    idr_vec4 aabb[2];
    
    glPushMatrix();
    glLoadIdentity();

    entities[0].object->bounding_box(& aabb[0], & aabb[1]);
    entities[0].color[0] = 0xbb;
    entities[0].color[1] = 0x44;
    entities[0].color[2] = 0x00;
    entities[0].color[3] = 0xff;

    const idr_vec4 center = (aabb[0] + aabb[1]) / 2.0;
    const float size = (aabb[0] - aabb[1]).magnitude();


    glRotatef(x_angle, 1.0f, 0.0f, 0.0f);
    glRotatef(y_angle, 0.0f, 1.0f, 0.0f);
#define MAX_SIZE 8.0f
    glScalef(MAX_SIZE / size, MAX_SIZE / size, MAX_SIZE / size);
    glTranslatef(-center[0], -center[1], -center[2]);

    glGetFloatv(GL_MODELVIEW_MATRIX, entities[0].transform);
    glPopMatrix();


    /* Create the transformations for the secondary objects in the scene.
     */
    for (i = 0; i < NUM_EXTRA_OBJS; i++) {
        glPushMatrix();
        glLoadIdentity();
        glTranslatef(extra_objs[i][0],
                     extra_objs[i][1], 
                     extra_objs[i][2]);

        glGetFloatv(GL_MODELVIEW_MATRIX, entities[i + 1].transform);

        glPopMatrix();

        (void) memcpy(entities[i + 1].color, extra_colors[i],
                      sizeof(extra_colors[0]));
    }




    glDepthMask(GL_TRUE);
    glEnable(GL_DEPTH_TEST);
    glClearColor(0.0f, 0.2f, 0.2f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    /* Position the light in the scene.
     */
    const idr_vec4 v((float)sin(DEGREES_TO_RADIANS(light_angle)),
                     (float)cos(DEGREES_TO_RADIANS(light_angle)),
                     0.0f, 1.0f);

    for (unsigned j = 0; j < 4; j++) {
        light[j] = light_move[j].dot4(v);
    }


    /* Draw the light.
     */
    glUseProgramObjectARB(0);

    const GLfloat l[4] = {
        light[0], light[1], light[2], 1.0
    };

    glPointSize(3.0);
    glBegin(GL_POINTS);
    glColor3ub(0xff, 0xff, 0x00);
    glVertex4fv(l);
    glEnd();

    glLightfv(GL_LIGHT0, GL_POSITION, l);


    glMatrixMode (GL_MODELVIEW);
    glLoadIdentity();
    gluLookAt(0.0f, 10.0f, 40.0f,
              0.0f, 0.0f, 0.0f,
              0.0f, 1.0f, 0.0f);

    GL_CHECK_ERROR();
    draw_scene(entities, 5, light);
    GL_CHECK_ERROR();

    glMatrixMode(GL_TEXTURE);
    glLoadIdentity();

    draw_HUD(font_vera, fps);
    GL_CHECK_ERROR();

    SDL_GL_SwapBuffers();
    frames++;

    GLuint t = SDL_GetTicks();
    if (t - t0 >= 5000) {
        GLfloat seconds = (GLfloat) (t - t0) / 1000.0f;
        fps = frames / seconds;
        t0 = t;
        frames = 0;
    }
}


static void dump_shader_messages(GLhandleARB vs, GLhandleARB fs,
                                 GLhandleARB p, const char *f, unsigned l)
{
    GLenum err = glGetError();
    if (err) {
        printf("%s:%u: Error: %s (0x%04x)\n", f, l, gluErrorString(err), err);
        printf("Program messages:\n");
        glsl_print_log(p);
        printf("\nVertex shader messages:\n");
        glsl_print_log(vs);
        printf("\nFragment shader messages:\n");
        glsl_print_log(fs);
    }
}

static void do_shader(struct entity *e, 
                      const char *vert_shader, const char *frag_shader)
{
    static const GLcharARB glsl_version[] = "#version 110\n";
    static const GLcharARB glsl_lit_shader[] = "#define lit_shader\n";
    static const GLcharARB glsl_amb_shader[] = "#define amb_shader\n";
    static const GLcharARB glsl_shv_shader[] = "#define shv_shader\n";
    const GLcharARB *vertex_code = glsl_load_shader_code(vert_shader);
    const GLcharARB *frag_code = glsl_load_shader_code(frag_shader);

    const GLcharARB *lit_vs_code[] = {
        glsl_version,
		glsl_lit_shader,
        vertex_code
    };

    const GLcharARB *lit_fs_code[] = {
        glsl_version,
		glsl_lit_shader, 
        frag_code
    };

    const GLcharARB *amb_vs_code[] = {
        glsl_version,
		glsl_amb_shader,
        vertex_code
    };

    const GLcharARB *amb_fs_code[] = {
        glsl_version,
		glsl_amb_shader, 
        frag_code
    };

    const GLcharARB *shv_vs_code[] = {
        glsl_version,
		glsl_shv_shader,
        vertex_code
    };

    GLhandleARB fs;
    GLhandleARB vs;
    GLhandleARB prog;

    glGetError();

    printf("Creating program for \"%s\" and \"%s\"...\n",
           vert_shader, frag_shader);

    printf("    lit shader mode...\n");
    vs = glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);
    glShaderSourceARB(vs, 3, lit_vs_code, NULL);
    glCompileShaderARB(vs);

    fs = glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);
    glShaderSourceARB(fs, 3, lit_fs_code, NULL);
    glCompileShaderARB(fs);

    prog = glCreateProgramObjectARB();
    glAttachObjectARB(prog, vs);
    glAttachObjectARB(prog, fs);
    glBindAttribLocationARB(prog, 1, "tangent");
    glLinkProgramARB(prog);

    dump_shader_messages(vs, fs, prog, __FILE__, __LINE__);
    e->lit_shader = new glsl_shader_program(prog, "lit shader");
    e->lit_shader->activate();
    e->lit_shader->uniform3("amb_color", 1, amb_color); 
    dump_shader_messages(vs, fs, prog, __FILE__, __LINE__);
    glUseProgramObjectARB(0);

    printf("    amb shader mode...\n");
    vs = glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);
    glShaderSourceARB(vs, 3, amb_vs_code, NULL);
    glCompileShaderARB(vs);

    fs = glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);
    glShaderSourceARB(fs, 3, amb_fs_code, NULL);
    glCompileShaderARB(fs);

    prog = glCreateProgramObjectARB();
    glAttachObjectARB(prog, vs);
    glAttachObjectARB(prog, fs);
    glBindAttribLocationARB(prog, 1, "tangent");
    glLinkProgramARB(prog);
    
    dump_shader_messages(vs, fs, prog, __FILE__, __LINE__);
    e->ambient_shader = new glsl_shader_program(prog, "amb shader");
    e->ambient_shader->activate();
    e->ambient_shader->uniform3("amb_color", 1, amb_color); 
    dump_shader_messages(vs, fs, prog, __FILE__, __LINE__);
    glUseProgramObjectARB(0);


    printf("    shv shader mode...\n");
    vs = glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);
    glShaderSourceARB(vs, 3, shv_vs_code, NULL);
    glCompileShaderARB(vs);

    prog = glCreateProgramObjectARB();
    glAttachObjectARB(prog, vs);
    glLinkProgramARB(prog);

    dump_shader_messages(vs, fs, prog, __FILE__, __LINE__);
    e->sv_shader = new glsl_shader_program(prog, "shv shader");
    e->sv_shader->activate();
    dump_shader_messages(vs, fs, prog, __FILE__, __LINE__);
    glUseProgramObjectARB(0);

    printf("Done.\n\n");

    delete vertex_code;
    delete frag_code;

    glUseProgramObjectARB(0);
}


static void Init(const char *filename)
{
    GLenum err = glewInit();

    if (err != GLEW_OK) {
        printf("GLew failed to initialize.\n");
        exit(1);
    }

    if (!GLEW_ARB_fragment_shader) {
        printf ("Sorry, this demo requires GL_ARB_fragment_shader\n");
        exit(1);
    }
    if (!GLEW_ARB_shader_objects) {
        printf ("Sorry, this demo requires GL_ARB_shader_objects\n");
        exit(1);
    }
    if (!GLEW_ARB_shading_language_100) {
        printf ("Sorry, this demo requires GL_ARB_shading_language_100\n");
        exit(1);
    }
    if (!GLEW_ARB_vertex_shader) {
        printf ("Sorry, this demo requires GL_ARB_vertex_shader\n");
        exit(1);
    }


    do_shader(& entities[0], "vertex_shader.glsl", "phong.glsl");
    entities[1] = entities[0];
    entities[2] = entities[0];
    entities[3] = entities[0];

    do_shader(& entities[4], "ground_vert.glsl", "ground.glsl");


    glEnable(GL_DEPTH_TEST);
    glEnable(GL_CULL_FACE);
	glClearStencil(0);
    GL_CHECK_ERROR();

    glClearColor(0.0f, 0.2f, 0.2f, 0.0f);

	if (filename == NULL) {
		filename = "Armadillo.ply.gz";
	}

    entities[0].object = new idr_ply_model(filename);
	if (!entities[0].object->model_good()) {
        entities[0].object = new idr_torus(0.8, 0.4, 11, 27);
    }

    entities[1].object = new idr_sphere(1.0, 17, 27);
    entities[2].object = entities[1].object;
    entities[3].object = entities[1].object;


    entities[4].object = new idr_plane(idr_vec4(0.0, -4.0, 0.0, 0.0),
				       idr_vec4(0.0, 1.0, 0.0, 0.0),
				       idr_vec4(1.0, 0.0, 0.0, 0.0));

    GLfloat fog_color[4] = { 0.0f, 0.2f, 0.2f, 0.0f };
    glFogf(GL_FOG_START, 30.0f);
    glFogf(GL_FOG_END, 68.0f);
    glFogf(GL_FOG_DENSITY, 0.05f);
    glFogfv(GL_FOG_COLOR, fog_color);


    printf("GL_RENDERER = %s\n", (const char *) glGetString(GL_RENDERER));
}


static void Idle(void)
{
    static Uint32 last_t = ~0;
    Uint32 t = SDL_GetTicks();


    if (last_t != (Uint32)~0) {
        float dt = (float) (t - last_t) / 1000.0f;

        if (anim) {
            x_angle += 45.0f * dt;
            y_angle += 60.0f * dt;
        }

        if (light_anim) {
            light_angle += 45.0f * dt;
        }
    }

    last_t = t;
}


static void Reshape(int w, int h, bool fullscreen)
{
    Uint32 mode_flags = SDL_OPENGL;
    width = w;
    height = h;

    mode_flags |= (fullscreen) ? SDL_FULLSCREEN : SDL_RESIZABLE;

    SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

    my_surf = SDL_SetVideoMode(width, height, 0, mode_flags);
    if (my_surf == NULL) {
        exit(1);
    }

    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    glFrustum(-1.0, 1.0, -1.0, 1.0, 5.0, 70.0);
}


static void Key(SDLKey sym)
{
    switch (sym) {
    case SDLK_ESCAPE:
        done = true;
        break;
            
    case 'f':
        if (is_full) {
            Reshape(win_width, win_height, false);
        }
        else {
            win_width = width;
            win_height = height;
            Reshape(1024, 768, true);
        }
        is_full = !is_full;
        break;

    case ' ':
    case 'a':
        anim = !anim;
        break;

    case 'l':
        light_anim = !light_anim;
        break;

    default:
        break;
    }
}


/**
 * Push an event each time the timer callback fires.
 */
Uint32 timer_callback(Uint32 interval, void *not_used)
{
    SDL_Event e;

    (void) not_used;

    e.type = SDL_USEREVENT;
    e.user.code = 0;
    e.user.data1 = NULL;
    e.user.data2 = NULL;
    SDL_PushEvent(& e);

    return interval;
}


int main(int argc, char *argv[])
{
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) < 0) {
        exit(1);
    }

    atexit(SDL_Quit);

    if (TTF_Init() < 0) {
        exit(1);
    }

    atexit(TTF_Quit);

    font_vera = TTF_OpenFont("Vera.ttf", 28);
    if (font_vera == NULL) {
        printf("Unable to open Vera at 28pt: %s\n", TTF_GetError());
        exit(1);
    }

    Reshape(width, height, false);
    Init(argv[1]);
//    done = true;

    SDL_TimerID timer_id = SDL_AddTimer(10, timer_callback, NULL);

    while (!done) {
        SDL_Event event;

        SDL_WaitEvent(&event);
        do {
            switch (event.type) {
            case SDL_VIDEORESIZE:
                Reshape(event.resize.w, event.resize.h, false);
                break;

            case SDL_QUIT:
                done = true;
                break;
                
            case SDL_KEYDOWN:
                if ((event.key.keysym.sym == SDLK_RSHIFT)
                    || (event.key.keysym.sym == SDLK_LSHIFT)) {
                    shift_status++;
                }
                else {
                    Key(event.key.keysym.sym);
                }
                break;

            case SDL_KEYUP:
                if ((event.key.keysym.sym == SDLK_RSHIFT)
                    || (event.key.keysym.sym == SDLK_LSHIFT)) {
                    shift_status--;
                }

                /* By clamping the shift status value to 0 we prevent some
                 * bugs when the program is started with one or both of the
                 * shift keys held down.
                 */
                if (shift_status < 0) {
                    shift_status = 0;
                }
                break;

            default:
                break;
            }
        } while (SDL_PollEvent(&event));


        Idle();

        Redisplay();
    }

    SDL_RemoveTimer(timer_id);

    delete entities[0].object;
    delete entities[1].object;
    delete entities[4].object;

    SDL_Quit();
    return 0;
}
