#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>
#include <SDL2/SDL.h>
#include <math.h>
#include "upng.h"
#include "display.h"
#include "vector.h"
#include "light.h"
#include "mesh.h"
#include "texture.h"
#include "triangle.h"
#include "array.h"
#include "matrix.h"

#define M_PI 3.1415926535

// Array of triangles that should be rendered frame by frame
triangle_t* triangles_to_render = NULL;

// Global variables for execution status and game loop
vec3_t camera_position = { .x = 0, .y = 0, .z = 0 };
mat4_t proj_matrix;

static enum cull_method cull_method;
static enum render_method render_method;

bool is_running = false;
int previous_frame_time = 0;


// Setup function to initialize variables and game objects
void setup(void){
    render_method = RENDER_WIRE;
    cull_method = CULL_BACKFACE;

    // Allocates the memory in bytes required to hold for color_buffer, using sizeof to allocate the # of bytes required
    color_buffer = (uint32_t*) malloc(sizeof(uint32_t) * window_width * window_height);

    // Creating a SDL texture that is used to display the color buffer
    color_buffer_texture = SDL_CreateTexture(
	    renderer,
	    SDL_PIXELFORMAT_RGBA32,
	    SDL_TEXTUREACCESS_STREAMING,
	    window_width,
	    window_height
	);

    // Initialize the perspective projection matrix
    float fov = M_PI / 3.0; // the same as 180 / 3 or 60 deg
    float aspect = (float)window_height / (float)window_width;
    float znear = 0.1;
    float zfar = 100.0;
    proj_matrix = mat4_make_perspective(fov, aspect, znear, zfar);
   
    // Load the hardcoded texture data from the static array
    //mesh_texture = (uint32_t*)REDBRICK_TEXTURE;
    //texture_width = 64;
    //texture_height = 64;

    // Loads the cube values in the mesh data structure
    //load_cube_mesh_data();
    load_png_texture_data("./assets/f117.png");
    load_obj_file_data("./assets/f117.obj");
    //load_obj_file_data("./assets/cube.obj");

}

void process_input(void){
    SDL_Event event;
    SDL_PollEvent(&event);
    
    switch (event.type) {
	case SDL_QUIT:
	    is_running = false;
	    break;
	case SDL_KEYDOWN:
	    if (event.key.keysym.sym == SDLK_ESCAPE)
		    is_running = false;
        if (event.key.keysym.sym == SDLK_1) {
            render_method = RENDER_WIRE_VERTEX;
        }
        if (event.key.keysym.sym == SDLK_2) {
            render_method = RENDER_WIRE;
        }
        if (event.key.keysym.sym == SDLK_3) {
            render_method = RENDER_FILL_TRIANGLE;
        }
        if (event.key.keysym.sym == SDLK_4) {
            render_method = RENDER_FILL_TRIANGLE_WIRE;
        }
        if (event.key.keysym.sym == SDLK_5) {
            render_method = RENDER_TEXTURED;
        }
        if (event.key.keysym.sym == SDLK_6) {
            render_method = RENDER_TEXTURED_WIRE;
        }
        if (event.key.keysym.sym == SDLK_c) {
            cull_method = CULL_BACKFACE;
        }
        if (event.key.keysym.sym == SDLK_d) {
            cull_method = CULL_NONE;
        }
	  break;
    }
}

/*
 * Function that receives a 3D vector and returns a 2D point
 */
/*
vec2_t project(vec3_t point) {
    vec2_t projected_point = {
	.x = (fov_factor * point.x) / point.z, 
	.y = (fov_factor * point.y) / point.z
    };

    return projected_point;
}*/
// Loads the cube values in the mesh data structure}

void update(void){
    //while (!SDL_TICKS_PASSED(SDL_GetTicks(), previous_frame_time + FRAME_TARGET_TIME));
    int time_to_wait = FRAME_TARGET_TIME - (SDL_GetTicks() - previous_frame_time);
    
    if (time_to_wait > 0 && time_to_wait <= FRAME_TARGET_TIME) {
        SDL_Delay(time_to_wait);
    }

    previous_frame_time = SDL_GetTicks();

    // Initialize the array of triangles to render
    triangles_to_render = NULL;

    // Change the mesh scale/rotation values per animation frame
    mesh.rotation.x += 0.01;
    mesh.rotation.y += 0.01;
    mesh.rotation.z += 0.004;

    //mesh.scale.x += 0.01;
    //mesh.scale.y += 0.01;
    mesh.translation.z = 5.0;

    // Create a scale and translation matrix that will be used to multiply the mesh vertices
    mat4_t scale_matrix = mat4_make_scale(mesh.scale.x, mesh.scale.y, mesh.scale.z);
    mat4_t translation_matrix = mat4_make_translation(mesh.translation.x, mesh.translation.y, mesh.translation.z);
    mat4_t rotation_matrix_x = mat4_make_rotation_x(mesh.rotation.x);
    mat4_t rotation_matrix_y = mat4_make_rotation_y(mesh.rotation.y);
    mat4_t rotation_matrix_z = mat4_make_rotation_z(mesh.rotation.z);

    // Loop all triangle faces of our mesh
    int num_faces = array_length(mesh.faces);
    for (int i = 0; i < num_faces; i++) {
        face_t mesh_face = mesh.faces[i];
        
        vec3_t face_vertices[3];
        face_vertices[0] = mesh.vertices[mesh_face.a];
        face_vertices[1] = mesh.vertices[mesh_face.b];
        face_vertices[2] = mesh.vertices[mesh_face.c];
    
        //triangle_t projected_triangle;

        vec4_t transformed_vertices[3];

        // Loop all three vertices of this current face and apply transformations
        for (int j = 0; j < 3; j++) {
            vec4_t transformed_vertex = vec4_from_vec3(face_vertices[j]);

            // Create a World Matrix combining the scale, rotation and translation matrices
            mat4_t world_matrix = mat4_identity();

            // Order matters, first scale, then rotate, then translate. [T] * [R] * [S] * v
            world_matrix = mat4_mul_mat4(scale_matrix, world_matrix);
            world_matrix = mat4_mul_mat4(rotation_matrix_z, world_matrix);
            world_matrix = mat4_mul_mat4(rotation_matrix_y, world_matrix);
            world_matrix = mat4_mul_mat4(rotation_matrix_x, world_matrix);
            world_matrix = mat4_mul_mat4(translation_matrix, world_matrix);

            // Multiply the world matrix by the original vector
            transformed_vertex = mat4_mul_vec4(world_matrix, transformed_vertex);

            // Save the transformed vertex in the array of transformed vertices
            transformed_vertices[j] = transformed_vertex;
        }

            vec3_t vector_a = vec3_from_vec4(transformed_vertices[0]); /*   A   */
            vec3_t vector_b = vec3_from_vec4(transformed_vertices[1]); /*  / \  */
            vec3_t vector_c = vec3_from_vec4(transformed_vertices[2]); /* C---B */

            // Get the vector subtraction of B-A and C-A
            vec3_t vector_ab = vec3_sub(vector_b, vector_a);
            vec3_t vector_ac = vec3_sub(vector_c, vector_a);
            vec3_normalize(&vector_ab);
            vec3_normalize(&vector_ac);

            // Compute the face normal (using cross product to find perpendicular)
            vec3_t normal = vec3_cross(vector_ab, vector_ac);
   
            // Normalize the face normal vector
            vec3_normalize(&normal);

            // Find the vector between a point in the triangle and the camera origin
            vec3_t camera_ray = vec3_sub(camera_position, vector_a);

            // Calculate how aligned the camera ray is with the face normal (using dot product)
            float dot_normal_camera = vec3_dot(normal, camera_ray);
        
        if (cull_method == CULL_BACKFACE) {
            // If dot product is negative, continue and bypass this for-loop iteration
            // We do not want to push a face that has a negative dot product
            // This bypasses triangles that are looking away from the camera
            if (dot_normal_camera < 0) {
                continue;
            }
        }

        vec4_t projected_points[3];

        // Loop all three vertices to perform projection
        for (int j = 0; j < 3; j++) {
            // Project the current vertex
            projected_points[j] = mat4_mul_vec4_project(proj_matrix, transformed_vertices[j]);

            // scale into the view
            projected_points[j].x *= (window_width / 2.0);
            projected_points[j].y *= (window_height / 2.0);

            // Invert the y values to account for flipped screen y coordinates
            projected_points[j].y *= -1;

            // translate the projected points to the middle of the screen
            projected_points[j].x += (window_width / 2.0);
            projected_points[j].y += (window_height / 2.0);

        }

        // Calculate the avg_depth for each face based on the vertices after transformation
        float avg_depth = (transformed_vertices[0].z + transformed_vertices[1].z + transformed_vertices[2].z) / 3.0;

        // calculate the shade intensity based on how aligned is the face normal and the inverse of the light (ray)direction
        float light_intensity_factor = -vec3_dot(normal, light.direction);

        // calculate the triangle colour based on the light angle
        uint32_t triangle_color = light_apply_intensity(mesh_face.color, light_intensity_factor);

        triangle_t projected_triangle = {
            .points = {
                { projected_points[0].x, projected_points[0].y, projected_points[0].z, projected_points[0].w },
                { projected_points[1].x, projected_points[1].y, projected_points[1].z, projected_points[1].w },
                { projected_points[2].x, projected_points[2].y, projected_points[2].z, projected_points[2].w },
            },
            .texcoords = {
                { mesh_face.a_uv.u, mesh_face.a_uv.v },
                { mesh_face.b_uv.u, mesh_face.b_uv.v },
                { mesh_face.c_uv.u, mesh_face.c_uv.v }
            },
            // every triangle has a colour (as does each face)
            .color = triangle_color,
            .avg_depth = avg_depth
        };

        // Save the projected triangle in the array of triangles to render
        //triangles_to_render[i] = projected_triangle; 
        array_push(triangles_to_render, projected_triangle);
    }

    // sort avg_depth of triangle faces for all z vertices per triangle
    // painters algorithm
    avg_depth_bubble_sort(triangles_to_render, array_length(triangles_to_render));
    /*for (int t = 0; t < array_length(triangles_to_render); t++) {
        printf("%.3f\n",triangles_to_render[t].avg_depth);
    }*/
}

void render(void){
    SDL_RenderClear(renderer);

    draw_grid();

    //Loop all projected points and render them
    int num_of_triangles = array_length(triangles_to_render);
    for (int i = 0; i < num_of_triangles; i++){
        triangle_t triangle = triangles_to_render[i];
    

        // Draw filled triangle
        if (render_method == RENDER_FILL_TRIANGLE || render_method == RENDER_FILL_TRIANGLE_WIRE) {
            draw_filled_triangle(
                triangle.points[0].x, triangle.points[0].y, // vertex A
                triangle.points[1].x, triangle.points[1].y, // vertex B
                triangle.points[2].x, triangle.points[2].y, // vertex C                                                        
                triangle.color
            );
        };

        // Draw textured triangle
        if (render_method == RENDER_TEXTURED || render_method == RENDER_TEXTURED_WIRE) {
            draw_textured_triangle(
                triangle.points[0].x, triangle.points[0].y, triangle.points[0].z, triangle.points[0].w, triangle.texcoords[0].u, triangle.texcoords[0].v,  // vertex A
                triangle.points[1].x, triangle.points[1].y, triangle.points[1].z, triangle.points[1].w, triangle.texcoords[1].u, triangle.texcoords[1].v,  // vertex B
                triangle.points[2].x, triangle.points[2].y, triangle.points[2].z, triangle.points[2].w, triangle.texcoords[2].u, triangle.texcoords[2].v,   // vertex C
                mesh_texture                                                      
            );

        }

        // Draw triangle wireframe
        if (render_method == RENDER_WIRE || render_method == RENDER_WIRE_VERTEX || render_method == RENDER_FILL_TRIANGLE_WIRE || render_method == RENDER_TEXTURED_WIRE) {
            draw_triangle(
                triangle.points[0].x, triangle.points[0].y, // vertex A
                triangle.points[1].x, triangle.points[1].y, // vertex B
                triangle.points[2].x, triangle.points[2].y, // vertex C
                0xFFFFFFFF
            );
        }

        // Draw triangle vertex points
        if (render_method == RENDER_WIRE_VERTEX) {
            draw_rect(triangle.points[0].x - 3, triangle.points[0].y - 3, 6, 6, 0xFF0000FF); // vertex A
            draw_rect(triangle.points[1].x - 3, triangle.points[1].y - 3, 6, 6, 0xFF0000FF); // vertex B
            draw_rect(triangle.points[2].x - 3, triangle.points[2].y - 3, 6, 6, 0xFF0000FF); // vertex C
        } 
    }   

    // draw_triangle(300, 100, 50, 400, 500, 700, 0xFF00FF00);
    array_free(triangles_to_render);

    render_color_buffer();
    
    clear_color_buffer(0xFF000000);

    SDL_RenderPresent(renderer);
}

// Free memory that was dynamically allocated
void free_resources(void) {
    free(color_buffer);
    upng_free(png_texture);
    array_free(mesh.faces);
    array_free(mesh.vertices);
}

int main(void) {    
    is_running = initialize_window();


    setup();
    
    while (is_running) {
	    process_input();
	    update();
	    render();
    }


    destroy_window();
    free_resources();

    return 0;
}
