/* Computer Graphics, Assignment, Depth-sorting and transparency
 * Description ..... Depth-Sorting
 * Date ............ 23.11.2009
 * Created by ...... Paul Melis
 *
 * Student name .... Sander van Veen & Taddeus Kroes
 * Student email ... sandervv@gmail.com & taddeuskroes@hotmail.com
 * Collegekaart .... 6169676 & 6054129
 * Date ............ 01.12.2010
 */

#if defined(__GNUC__)
#define glCheckError(s) { GLenum error; if ((error=glGetError())) \
  fprintf(stderr, "* GL error - %s (%s; %s, line %d): %s\n", \
  (s), __FUNCTION__,__FILE__,__LINE__, gluErrorString(error)); }
#else                           /* __GNUC__ */
#define glCheckError(s) { GLenum error; if ((error=glGetError())) \
  fprintf(stderr, "* GL error - %s (line %d): %s\n", \
  (s), __LINE__, gluErrorString(error)); }
#endif                          /* __GNUC__ */

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <GL/glut.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <math.h>
#include "polys.h"
#include "readpng.h"
#include "geometry.h"
#include "bsp.h"

#define DOTPROD_TRESHOLD 1.0e-3

struct texture_data
{
    const char *filename;
    int         contains_transparency;
};

struct texture_data texture_files[] =
{
    // File name, transparency flag (the flag will be set during image loading)
    { "textures/wall_bricks_only.png", 0 },
    { "textures/wall_with_window.png", 0 },
    { "textures/roof2.png", 0 },
    { "textures/grass.png", 0 },
    { "textures/tree.png", 0 },
    { "textures/sky.png", 0 },
    { "textures/hek2.png", 0 },
    { NULL, 0 },
};

GLuint  *texture_names;
int     num_textures;

polys   *scene_polygons;
polys   *scene_opaque_polygons;
polys   *scene_transparent_polygons;
polys   *polylistSkydome;

BSPNode *bsp_root;

int     show_textures = 0;
int     show_polygons_as_lines = 0;
int     do_depth_sorting = 0;
int     sort_using_rays = 0;
int     draw_opaque_polygons = 1;

vec3    camPosition;
float   camDistance = 35.0;
float   camRotZ = -45.0, camAzimuth = 20.0;
float   saved_camRotZ, saved_camAzimuth, saved_camDistance;
int     mouse_mode = 0;
int     mx, my;

GLfloat mat_sun[] = { 1.0, 1.0, 0.5, 1.0 };
GLfloat mat_no_sun[] = { 0.0, 0.0, 0.0, 1.0 };
GLfloat mat_ambient[] = { 0.4, 0.4, 0.4, 1.0 };
GLfloat mat_diffuse[] = { 1, 1, 1, 1.0 };
GLfloat mat_specular[] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat mat_no_diffuse[] = { 1.0, 0.0, 0.0, 1.0 };
GLfloat mat_no_specular[] = { 0.0, 0.0, 0.0, 1.0 };
GLfloat mat_shininess[] = { 50.0 };
GLfloat mat_no_shininess[] = { 0.0 };

GLfloat light_position[] = { -8.0, -8.0, 8.0, 0.0 };
GLfloat light_ambient[] = { 0.4, 0.4, 0.4, 0.0 };
GLfloat light_diffuse[] = { 0.6, 0.6, 0.6, 0.0 };

// Rough center points of the objects in the scene.
// Note: these positions are only used for rotating around them
// with the camera, not to place objects in the scene.

#define NUM_OBJECTS 4
vec3 object_positions[NUM_OBJECTS] =
{
    /* house, goldfish, tree, somewhere at the side of the house */
    {0.0, 0.0, 5.0},
    {0.0, 0.0, 4.5},
    {5.0, 18.0, 10.0},
    {0.0, 10.0, 0.0},
};

// Determines around which object the camera rotates
int current_object = 0;

void
InitializePolygonlists(void)
{
    polys   *model_polygons;
    poly    p;

    scene_polygons = CreatePolylist(128);
    model_polygons = CreatePolylist(128);

    // Load the models from .obj files and create a single polylist
    // (scene_polygons) to hold all polygons

    // House
    loadPolygonalObject(model_polygons, "house.obj", texture_names, num_textures,
        1.0, 0.0, 0.0, 0.0);
    AppendPolylist(scene_polygons, model_polygons);

    // Ground
    ClearPolylist(model_polygons);
    loadPolygonalObject(model_polygons, "ground.obj", texture_names, num_textures,
        1.0, 0.0, 0.0, 0.0);
    AppendPolylist(scene_polygons, model_polygons);

    // Table
    ClearPolylist(model_polygons);
    loadPolygonalObject(model_polygons, "table.obj", texture_names, num_textures,
        1.0, 0.0, 0.0, 0.0);
    AppendPolylist(scene_polygons, model_polygons);

    // Tree
    ClearPolylist(model_polygons);
    loadPolygonalObject(model_polygons, "tree.obj", texture_names, num_textures,
        0.6, 5.0, 16.0, 0.0);
    AppendPolylist(scene_polygons, model_polygons);

    // Fish bowl
    ClearPolylist(model_polygons);
    loadPolygonalObject(model_polygons, "fishbowl.obj", texture_names, num_textures,
        1.0, 0.0, 0.0, 0.0);
    AppendPolylist(scene_polygons, model_polygons);

    // Goldfish
    ClearPolylist(model_polygons);
    loadPolygonalObject(model_polygons, "goldfish.obj", texture_names, num_textures,
        1.0, 0.0, 0.0, 0.0);
    AppendPolylist(scene_polygons, model_polygons);

    // Fence
    ClearPolylist(model_polygons);
    loadPolygonalObject(model_polygons, "hek.obj", texture_names, num_textures,
        1.0, 0.0, 0.0, 0.0);
    AppendPolylist(scene_polygons, model_polygons);

    // A transparent cow (moooh!)
    ClearPolylist(model_polygons);
    loadPolygonalObject(model_polygons, "cow.obj", texture_names, num_textures,
        10.0, -3.0, -15.0, 0.0);
    AppendPolylist(scene_polygons, model_polygons);

    // The skydome
    polylistSkydome = CreatePolylist(1000);
    createHemisphere(polylistSkydome, 250,  0, 0, 0,  1, 1, 1);
    for (int i = 0; i < polylistSkydome->length; i++)
        polylistSkydome->items[i].opengl_texture_name = texture_names[5];

    // Done!
    printf("%d polygons in the scene\n", scene_polygons->length);

    //
    // Sort polygons by type (opaque versus transparent)
    //

    // Estimate needed list size
    scene_opaque_polygons = CreatePolylist(scene_polygons->length/2);
    scene_transparent_polygons = CreatePolylist(scene_polygons->length/2);

    for (int i = 0; i < scene_polygons->length; i++)
    {
        p = scene_polygons->items[i];

        if (
            // Non-textured transparent polygon
            (p.texture_id == -1 && p.color[3] < 1.0)
            ||
            // Textured transparent polygon
            (p.texture_id >= 0 && texture_files[p.texture_id].contains_transparency)
            )
        {
            // We split quads into two triangles, so that all transparent
            // geometry is defined with triangles only. This makes the
            // splitting during BSP construction much easier as
            // we only have to handle one type of primitive that can be
            // split in only a few ways.
            if (p.points == 4)
            {
                poly q = p;

                // q is defined by points 0, 1 and 2 of p
                q.points = 3;
                AddPolyToPolylist(scene_transparent_polygons, q);

                // second q is defined by points 0, 2 and 3 of p
                q.pts[1] = p.pts[2];
                q.tcoord[1] = p.tcoord[2];
                q.pts[2] = p.pts[3];
                q.tcoord[2] = p.tcoord[3];
                AddPolyToPolylist(scene_transparent_polygons, q);
            }
            else if (p.points == 3)
                AddPolyToPolylist(scene_transparent_polygons, p);
            else
                printf("Skipping polygon with %d points, only quads or triangles allowed\n", p.points);
        }
        else
        {
            // Opaque polygon
            AddPolyToPolylist(scene_opaque_polygons, p);
        }
    }

    printf("%d opaque polygons\n", scene_opaque_polygons->length);
    printf("%d transparent polygons\n", scene_transparent_polygons->length);

    // Build BSP tree
    bsp_root = BuildBSPTree(scene_transparent_polygons);
}

void
InitGL(void)
{
    GLvoid  *image_data;
    int     i, width, height;
    int     has_alpha_channel;

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(45.0f, (GLfloat) 800 / (GLfloat) 600, 0.1f, 1000.0f);

    glClearColor(0.7, 0.7, 1, 1);
    glDepthFunc(GL_LEQUAL);
    glEnable(GL_DEPTH_TEST);
    glDisable(GL_CULL_FACE);        // We want 2-sided geometry

    glEnable(GL_COLOR_MATERIAL);
    glMaterialfv(GL_FRONT, GL_AMBIENT, mat_ambient);
    glMaterialfv(GL_FRONT, GL_DIFFUSE, mat_diffuse);
    glMaterialfv(GL_FRONT, GL_SPECULAR, mat_no_specular);
    glMaterialfv(GL_FRONT, GL_SHININESS, mat_no_shininess);

    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0);
    glLightfv(GL_LIGHT0, GL_AMBIENT, light_ambient);
    glLightfv(GL_LIGHT0, GL_DIFFUSE, light_diffuse);

    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    // Using GL_REPLACE will effectively disable lighting.
    // The default mode, GL_MODULATE, will use the texture color to
    // module the shaded material color, so the end result still
    // contains the shading.
    //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);

    // generate OpenGL texture names

    num_textures = 0;
    for (i = 0; texture_files[i].filename != NULL; i++)
        num_textures++;

    texture_names = malloc(num_textures * sizeof(GLuint));
    glGenTextures(num_textures, texture_names);
    glCheckError("glGenTextures");

    // create textures from image files

    GLint   texture_internal_format;
    GLenum  texture_format, texture_type;

    for (i = 0; i < num_textures; i++)
    {
        image_data = (GLvoid *) readPNGFile(texture_files[i].filename, &width, &height, &has_alpha_channel);
        if (image_data)
        {
            printf("texture %d (OpenGL name = %d): %s, %d x %d\n", i,
                texture_names[i], texture_files[i].filename, width, height);

            if (has_alpha_channel)
            {
                // Convert the RGB image to RGBA, by replacing
                // red pixels with transparent ones

                printf("... contains alpha channel\n");

                texture_files[i].contains_transparency = 1;

                texture_internal_format = GL_RGBA8;
                texture_format = GL_RGBA;
                texture_type = GL_UNSIGNED_BYTE;
            }
            else
            {
                texture_internal_format = GL_RGB8;
                texture_format = GL_RGB;
                texture_type = GL_UNSIGNED_BYTE;
            }

            glBindTexture(GL_TEXTURE_2D, texture_names[i]);
            glCheckError("glBindTexture");

#if 0
            // No mipmapping
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
            glCheckError("glTexParameteri");

            glTexImage2D(GL_TEXTURE_2D, 0, texture_internal_format,
                width, height, 0, texture_format, texture_type, image_data);
            glCheckError("glTexImage2D");
#else
            // Mipmapping
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
            glCheckError("glTexParameteri");

            gluBuild2DMipmaps(GL_TEXTURE_2D, texture_internal_format, width, height,
                texture_format, texture_type, image_data);
            glCheckError("gluBuild2DMipmaps");
#endif

            // Free the image data, as OpenGL will have made its internal
            // copy by now
            free(image_data);
        }
        else
        {
            perror(texture_files[i].filename);
            exit(0);
        }
    }
}

void
ReSizeGLScene(int Width, int Height)
{
    if (Height == 0)
        Height = 1;

    glViewport(0, 0, Width, Height);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(45.0f, (GLfloat) Width / (GLfloat) Height, 0.1f, 1000.0f);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}

// Draw a single polygon, taking into account the texture
// and transparency properties of the polygon
void
DrawPolygon(poly p)
{
    if (p.opengl_texture_name >= 0)
    {
        // Textured
        glColor3f(p.color[0], p.color[1], p.color[2]);
        glBindTexture(GL_TEXTURE_2D, p.opengl_texture_name);
    }
    else
    {
        // Untextured (but colored and possibly with alpha transparency)
        glColor4f(p.color[0], p.color[1], p.color[2], p.color[3]);
        glBindTexture(GL_TEXTURE_2D, 0);
    }

    glBegin(GL_POLYGON);
    for (int j = 0; j < p.points; j++)
    {
        glNormal3f(p.normal[j].x, p.normal[j].y, p.normal[j].z);
        glTexCoord2f(p.tcoord[j].x, p.tcoord[j].y);
        glVertex3f(p.pts[j].x, p.pts[j].y, p.pts[j].z);
    }
    glEnd();
}

// Draw all the polygons in a list
void
DrawPolylist(polys * list)
{
    int i;

    for (i = 0; i < list->length; i++)
        DrawPolygon(list->items[i]);
}

// Use the BSP tree to draw transparent geometry in the correct
// order, based on the given eye position
void
DrawBSPTree(BSPNode *node, vec3 eye_position)
{
	// Check if the tree is empty
	if( !node )
		return;
	
	poly p = node->polygon;
	float dot = v3_dotprod(v3_subtract(eye_position, p.pts[0]), p.normal[0]);

	// Draw the tree on the other side of the polygon from the eye point, then
	// the polygon itself and finally the tree on the same side of the polygon.
	if( dot > DOTPROD_TRESHOLD )
	{
		DrawBSPTree(node->right_child, eye_position);
		DrawPolygon(p);
		DrawBSPTree(node->left_child, eye_position);
	}
	else 
	{
		DrawBSPTree(node->left_child, eye_position);
		
		// If the eye position is on the plane, the plane is invisible and
		// should not be drawn. This is determined using a treshold (the
		// same as in bsp.c)
		if( dot < -DOTPROD_TRESHOLD )
			DrawPolygon(p);
		
		DrawBSPTree(node->right_child, eye_position);
	}
}

void
SetupCamera(void)
{
    float   cx, cy, cz;
    float   t;
    float   beta, gamma;
    vec3    op;

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    // Verbose, but straightforward way, of positioning the camera.
    // Assume the camera's final position is (cx, cy, cz) and Z is up.
    // Start with c being (camDistance, 0, 0)
    // First rotate around Y, then around Z.
    // Now we have c at the given distance from the origin, with specified rotation angles.

    // degrees -> radians
    beta = camAzimuth / 180.0 * 3.1415926535;
    gamma = camRotZ / 180.0 * 3.1415926535;

    cx = camDistance;
    cy = cz = 0.0;

    // Rotate around Y
    t = cx;
    cx = cx * cos(beta) - cz * sin(beta);
    cz = t * sin(beta) + cz * cos(beta);

    // Rotate around Z
    t = cx;
    cx = cx * cos(gamma) + cy * sin(gamma);
    cy = t * sin(gamma) + cy * cos(gamma);

    // Translate view to rotate around the selected object
    op = object_positions[current_object];
    gluLookAt(cx+op.x, cy+op.y, cz+op.z, op.x, op.y, op.z, 0.0, 0.0, 1.0);

    camPosition.x = cx + op.x;
    camPosition.y = cy + op.y;
    camPosition.z = cz + op.z;
}

void
DrawGLScene(void)
{
    float   tx, ty, tz;
    float   a, rx, ry, rz;

    glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);

    // set camera based on previous mouse input
    SetupCamera();

    // Set the light position. We do that after setting up the
    // camera so that the light position will keep its world position.
    glLightfv(GL_LIGHT0, GL_POSITION, light_position);

    // Draw the scene

    if (show_polygons_as_lines)
    {
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
        glDisable(GL_TEXTURE_2D);
    }
    else
    {
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

        if (show_textures)
        {
            glEnable(GL_BLEND);
            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
            glEnable(GL_TEXTURE_2D);
        }
        else
            glDisable(GL_TEXTURE_2D);
    }

    if (draw_opaque_polygons)
    {
        DrawPolylist(scene_opaque_polygons);

        // Draw the skydome with lighting turned off

        glPushAttrib(GL_LIGHTING_BIT);
        glDisable(GL_LIGHTING);

        glPushMatrix();

        glRotatef(90, 1, 0, 0);
        DrawPolylist(polylistSkydome);

        glPopMatrix();

        glPopAttrib();
    }

    // XXX use BSP to draw

    DrawBSPTree(bsp_root, camPosition);

#if 0
    // Axes
    glPushAttrib(GL_LIGHTING_BIT);
    glDisable(GL_LIGHTING);

    glBegin(GL_LINES);
    glColor3f(1,0,0);
    glVertex3f(0,0,0);
    glVertex3f(1,0,0);
    glColor3f(0,1,0);
    glVertex3f(0,0,0);
    glVertex3f(0,1,0);
    glColor3f(0,0,1);
    glVertex3f(0,0,0);
    glVertex3f(0,0,1);
    glEnd();

    glPopAttrib();
#endif

    // Finally, swap back and front buffers
    glutSwapBuffers();
}

void
keyPressed(unsigned char key, int x, int y)
{
    (void)x;
    (void)y;

    switch (key)
    {
        case 'o':
            draw_opaque_polygons = 1 - draw_opaque_polygons;
            glutPostRedisplay();
            break;
        case 't':
            show_textures = 1 - show_textures;
            glutPostRedisplay();
            break;
        case 'l':
            show_polygons_as_lines = 1;
            glutPostRedisplay();
            break;
        case 'p':
            show_polygons_as_lines = 0;
            glutPostRedisplay();
            break;
        case 'd':
            do_depth_sorting = 1 - do_depth_sorting;
            printf("Depth sorting now %s\n", do_depth_sorting ? "on" : "off");
            glutPostRedisplay();
            break;
        case 'r':
            sort_using_rays = 1 - sort_using_rays;
            printf("Depth sorting using rays now %s\n", sort_using_rays ? "on" : "off");
            glutPostRedisplay();
            break;
        case 'v':
            current_object = (current_object + 1) % NUM_OBJECTS;
            glutPostRedisplay();
            break;
        case 'q':
            exit(-1);
    }
}

void
specialKeyPressed(int key, int x, int y)
{
    (void)x;
    (void)y;
    (void)key;

    DrawGLScene();
}

static void
mouseFunc(int button, int state, int x, int y)
{
    // guard against both left and right buttons being pressed at the same time,
    // by only responding when a mouse button is pressed while another one
    // hasn't been pressed yet
    if (state == GLUT_DOWN && mouse_mode == 0)
    {
        if (button == GLUT_LEFT_BUTTON)
        {
            mouse_mode = GLUT_LEFT_BUTTON;

            saved_camRotZ = camRotZ;
            saved_camAzimuth = camAzimuth;

            mx = x;
            my = y;
        } else if (button == GLUT_RIGHT_BUTTON)
        {
            mouse_mode = GLUT_RIGHT_BUTTON;

            saved_camDistance = camDistance;

            my = y;
        }
    } else if (state == GLUT_UP && button == mouse_mode)
    {
        // pressed button released
        mouse_mode = 0;
    }

}

static void
motionFunc(int x, int y)
{
    int dx, dy;

    if (mouse_mode == GLUT_LEFT_BUTTON)
    {
        dx = mx - x;
        dy = my - y;

        camRotZ = saved_camRotZ + dx * 0.25;
        camAzimuth = saved_camAzimuth - dy * 0.25;

        if (camAzimuth > 89.99)
            camAzimuth = 89.99;
        else if (camAzimuth < -89.99)
            camAzimuth = -89.99;
    }
    else if (mouse_mode == GLUT_RIGHT_BUTTON)
    {
        dy = my - y;

        camDistance = saved_camDistance - dy * 0.5;
        if (camDistance < 0.1)
            camDistance = 0.1;
        else if (camDistance > 1000.0)
            camDistance = 1000.0;
    }
}

int
main(int argc, char **argv)
{
    glutInit(&argc, argv);

    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_ALPHA | GLUT_DEPTH);
    glutInitWindowSize(800, 600);
    glutInitWindowPosition(0, 0);
    glutCreateWindow("OpenGL Framework");

    glutDisplayFunc(&DrawGLScene);
    glutIdleFunc(&DrawGLScene);
    glutReshapeFunc(&ReSizeGLScene);
    glutSpecialFunc(&specialKeyPressed);
    glutKeyboardFunc(&keyPressed);
    glutMouseFunc(&mouseFunc);
    glutMotionFunc(&motionFunc);

    InitGL();
    InitializePolygonlists();

    glutMainLoop();

    return 1;
}
