


/* On macOS, compile with...
    clang 690mainFrustum.c 040pixel.o -lglfw -framework OpenGL -framework Cocoa -framework IOKit
On Ubuntu, compile with...
    cc 690mainFrustum.c 040pixel.o -lglfw -lGL -lm -ldl
*/
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <GLFW/glfw3.h>
#include "040pixel.h"

#include "240vector.c"
#include "280matrix.c"
#include "300isometry.c"
#include "300camera.c"
#include "660ray.c"
#include "660body.c"
#include "660sphere.c"
#include "670half.c"
#include "680and.c"

#define SCREENWIDTH 512
#define SCREENHEIGHT 512



/*** ARTWORK ******************************************************************/

camCamera camera;
double cameraTarget[3] = {0.0, 0.0, 0.0};
double cameraRho = 10.0, cameraPhi = M_PI / 3.0, cameraTheta = M_PI / 3.0;

/* Four spheres and one half space. */
#define BODYNUM 11
bodyBody bodies[BODYNUM];

int initializeArtwork(void) {
	/* Camera. */
    camSetProjectionType(&camera, camPERSPECTIVE);
    camSetFrustum(
        &camera, M_PI / 6.0, cameraRho, 10.0, SCREENWIDTH, SCREENHEIGHT);
    camLookAt(&camera, cameraTarget, cameraRho, cameraPhi, cameraTheta);
    /* Initialize eleven bodies. */
    int error;
    for (int i = 0; i < 6; i += 1) {
    	error = bodyInitialize(&bodies[i], halfDATASIZE, halfGetIntersection);
    	if (error != 0) {
    		for (int j = 0; j < i; j += 1)
    			bodyFinalize(&bodies[j]);
    		return 2;
    	}
    }
    for (int i = 6; i < BODYNUM; i += 1) {
    	error = bodyInitialize(&bodies[i], andDATASIZE, andGetIntersection);
    	if (error != 0) {
    		for (int j = 0; j < i; j += 1)
    			bodyFinalize(&bodies[j]);
    		return 1;
    	}
    }
    double rot[3][3], transl[3], a[3], b[3];
    double i[3] = {1.0, 0.0, 0.0}, j[3] = {0.0, 1.0, 0.0};
    /* Near plane. */
    vec3Set(1.0, 0.0, 0.0, a);
    vec3Set(0.0, 1.0, 0.0, b);
    vec3Set(0.0, 0.0, -1.0, transl);
    mat3BasisRotation(i, j, a, b, rot);
    isoSetRotation(&(bodies[0].isometry), rot);
    isoSetTranslation(&(bodies[0].isometry), transl);
    /* Far plane. */
    vec3Set(1.0, 0.0, 0.0, a);
    vec3Set(0.0, -1.0, 0.0, b);
    vec3Set(0.0, 0.0, -4.0, transl);
    mat3BasisRotation(i, j, a, b, rot);
    isoSetRotation(&(bodies[1].isometry), rot);
    isoSetTranslation(&(bodies[1].isometry), transl);
    /* Left plane. */
    vec3Set(1.0 / sqrt(2.0), 0.0, 1.0 / sqrt(2.0), a);
    vec3Set(0.0, 1.0, 0.0, b);
    vec3Set(-1.0, 0.0, -1.0, transl);
    mat3BasisRotation(i, j, a, b, rot);
    isoSetRotation(&(bodies[2].isometry), rot);
    isoSetTranslation(&(bodies[2].isometry), transl);
    /* Right plane. */
    vec3Set(1.0 / sqrt(2.0), 0.0, -1.0 / sqrt(2.0), a);
    vec3Set(0.0, 1.0, 0.0, b);
    vec3Set(1.0, 0.0, -1.0, transl);
    mat3BasisRotation(i, j, a, b, rot);
    isoSetRotation(&(bodies[3].isometry), rot);
    isoSetTranslation(&(bodies[3].isometry), transl);
    /* Bottom plane. */
    vec3Set(1.0, 0.0, 0.0, a);
    vec3Set(0.0, 1.0 / sqrt(2.0), 1.0 / sqrt(2.0), b);
    vec3Set(0.0, -1.0, -1.0, transl);
    mat3BasisRotation(i, j, a, b, rot);
    isoSetRotation(&(bodies[4].isometry), rot);
    isoSetTranslation(&(bodies[4].isometry), transl);
    /* Top plane. */
    vec3Set(1.0, 0.0, 0.0, a);
    vec3Set(0.0, 1.0 / sqrt(2.0), -1.0 / sqrt(2.0), b);
    vec3Set(0.0, 1.0, -1.0, transl);
    mat3BasisRotation(i, j, a, b, rot);
    isoSetRotation(&(bodies[5].isometry), rot);
    isoSetTranslation(&(bodies[5].isometry), transl);
    /* Configure the AND bodies. */
    bodyBody **bods = (bodyBody **)(bodies[6].shapeData);
    bods[0] = &bodies[1];
    bods[1] = &bodies[0];
    bods = (bodyBody **)(bodies[7].shapeData);
    bods[0] = &bodies[2];
    bods[1] = &bodies[6];
    bods = (bodyBody **)(bodies[8].shapeData);
    bods[0] = &bodies[3];
    bods[1] = &bodies[7];
    bods = (bodyBody **)(bodies[9].shapeData);
    bods[0] = &bodies[4];
    bods[1] = &bodies[8];
    bods = (bodyBody **)(bodies[10].shapeData);
    bods[0] = &bodies[5];
    bods[1] = &bodies[9];
    return 0;
}

void finalizeArtwork(void) {
    for (int i = 0; i < BODYNUM; i += 1)
    	bodyFinalize(&bodies[i]);
}



/*** RENDERING ****************************************************************/

/* Given a ray x(t) = p + t u. Finds the color where that ray hits the scene (or 
the background), and outputs that color via the rgb parameter. */
void getSceneColor(const double p[3], const double u[3], double rgb[3]) {
	rayIntersection inter;
	bodyGetIntersection(&bodies[10], p, u, &inter);
    if (inter.start < rayEPSILON)
        vec3Set(0.0, 0.0, 0.0, rgb);
    else {
    	for (int k = 0; k < 3; k += 1)
			rgb[k] = fabs(inter.normal[k]);
    }
}

void render(void) {
    /* Build a 4x4 matrix that (along with homogeneous division) takes screen 
    coordinates (x0, x1, 0, 1) to the corresponding world coordinates. */
    double cMatrix[4][4], pInvMatrix[4][4], vInvMatrix[4][4];
    double cPInvMatrix[4][4], cPInvVInvMatrix[4][4];
    mat4InverseViewport(SCREENWIDTH, SCREENHEIGHT, vInvMatrix);
    isoGetHomogeneous(&(camera.isometry), cMatrix);
    if (camera.projectionType == camORTHOGRAPHIC)
        camGetInverseOrthographic(&camera, pInvMatrix);
    else
        camGetInversePerspective(&camera, pInvMatrix);
    mat4Compose(cMatrix, pInvMatrix, cPInvMatrix);
    mat4Compose(cPInvMatrix, vInvMatrix, cPInvVInvMatrix);
    /* Declare p and maybe compute u. */
    double p[4], u[3];
    if (camera.projectionType == camORTHOGRAPHIC) {
        double negK[3] = {0.0, 0.0, -1.0};
        isoRotateDirection(&(camera.isometry), negK, u);
    }
    /* Each screen point is chosen to be on the near plane. */
    double screen[4] = {0.0, 0.0, 0.0, 1.0};
    for (int i = 0; i < SCREENWIDTH; i += 1) {
        screen[0] = i;
        for (int j = 0; j < SCREENHEIGHT; j += 1) {
            screen[1] = j;
            /* Compute p and maybe also u. */
            mat4Transform(cPInvVInvMatrix, screen, p);
            vecScale(4, 1.0 / p[3], p, p);
            if (camera.projectionType == camPERSPECTIVE) {
                vecSubtract(3, p, camera.isometry.translation, u);
				vecUnit(3, u, u);
            }
            /* Set the pixel to the color of that ray x(t) = p + t * u. */
            double rgb[3];
            getSceneColor(p, u, rgb);
            pixSetRGB(i, j, rgb[0], rgb[1], rgb[2]);
        }
    }
}



/*** USER INTERFACE ***********************************************************/

void handleKey(
        int key, int shiftIsDown, int controlIsDown, int altOptionIsDown, 
        int superCommandIsDown) {
    if (key == GLFW_KEY_I)
        cameraPhi -= 0.1;
    else if (key == GLFW_KEY_K)
        cameraPhi += 0.1;
    else if (key == GLFW_KEY_J)
        cameraTheta -= 0.1;
    else if (key == GLFW_KEY_L)
        cameraTheta += 0.1;
    else if (key == GLFW_KEY_U)
        cameraRho *= 1.1;
    else if (key == GLFW_KEY_O)
        cameraRho *= 0.9;
    else if (key == GLFW_KEY_P) {
        if (camera.projectionType == camORTHOGRAPHIC)
            camSetProjectionType(&camera, camPERSPECTIVE);
        else
            camSetProjectionType(&camera, camORTHOGRAPHIC);
    }
    camSetFrustum(
        &camera, M_PI / 6.0, cameraRho, 10.0, SCREENWIDTH, SCREENHEIGHT);
    camLookAt(&camera, cameraTarget, cameraRho, cameraPhi, cameraTheta);
}

void handleTimeStep(double oldTime, double newTime) {
    if (floor(newTime) - floor(oldTime) >= 1.0)
        printf(
            "info: handleTimeStep: %f frames/s\n", 1.0 / (newTime - oldTime));
    double rot[3][3], axis[3] = {0.0, 0.0, 1.0};
    mat3AngleAxisRotation(newTime, axis, rot);
    //isoSetRotation(&(bodies[10].isometry), rot);
    render();
}

int main(void) {
    if (pixInitialize(SCREENWIDTH, SCREENHEIGHT, "Ray Tracing") != 0)
        return 1;
    if (initializeArtwork() != 0) {
        pixFinalize();
        return 2;
    }
    pixSetKeyDownHandler(handleKey);
    pixSetKeyRepeatHandler(handleKey);
    pixSetTimeStepHandler(handleTimeStep);
    pixRun();
    finalizeArtwork();
    pixFinalize();
    return 0;
}


