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

#define STEP 0.075

uint32_t colors[12] = {
    0xFF6A2EFF, // purple
    0xFFE9C46A, // sand yellow
    0xFF2A9D8F, // teal green
    0xFFF4A261, // orange
    0xFF264653, // dark blue-grey
    0xFFB5838D, // mauve
    0xFFD7263D, // red
    0xFF38A3A5, // turquoise
    0xFF757575, // grey
    0xFF70C1B3, // light green-blue
    0xFFFFC300, // yellow
    0xFF5D2E8C  // deep purple
};

triangle_t* triangles_to_render = NULL; //[N_MESH_FACES];

vec3_t camera_position = { 0, 0, 0 };

mat4_t proj_matrix;
light_t light = { .direction = { .x = 0, .y = 0, .z = 1 }};

bool is_running = false;

int cull_method = CULL_BACKFACE;
int render_method = RENDER_FILL_TRIANGLE;
bool sort_faces = true;

uint32_t previous_frame_time = 0;

void setup(void) {
    // Allocate the required memory in bytes to hold the color buffer
    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_ARGB8888,
        SDL_TEXTUREACCESS_STREAMING,
        window_width,
        window_height
    );

    float fov = M_PI / 3;
    float aspect = (float)window_height / (float)window_width;
    float znear = 0.0f;
    float zfar = 100.0f;

    proj_matrix = mat4_make_perspective(fov, aspect, znear, zfar);

    mesh_texture = (uint32_t*)REDBRICK_TEXTURE;
    texture_height = 64;
    texture_width = 64;

    // load cube mesh data
    //load_file("..\\assets\\f22.obj");
    load_cube_mesh_data();

}

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;
            //break;
            if (event.key.keysym.sym == SDLK_UP)
                mesh.rotation.z += STEP;
            if (event.key.keysym.sym == SDLK_LEFT)
                mesh.rotation.x += STEP;
            if (event.key.keysym.sym == SDLK_RIGHT)
                mesh.rotation.y += STEP;
            break;
        case SDL_KEYUP:
            SDL_Keycode key = event.key.keysym.sym;
            if (key == SDLK_c) {
                cull_method = !cull_method;
            }

            if (key == SDLK_s) {
                sort_faces = !sort_faces;
            }

            if (key == SDLK_1) {
                render_method = RENDER_WIRE_VERTEX;
            }
            if (key == SDLK_2) {
                render_method = RENDER_WIRE;
            }
            if (key == SDLK_3) {
                render_method = RENDER_FILL_TRIANGLE;
            }
            if (key == SDLK_4) {
                render_method = RENDER_FILL_TRIANGLE_LINE;
            }
            if (key == SDLK_5) {
                render_method = RENDER_TEXTURED;
            }
            if (key == SDLK_6) {
                render_method = RENDER_TEXTURED_LINE;
            }

            break;



    }
}

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

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);

    // only delay execution if we are running too fast
    if (time_to_wait > 0 && time_to_wait <= FRAME_TARGET_TIME) {
        SDL_Delay(time_to_wait);
    }

    previous_frame_time = SDL_GetTicks();

    //mesh.rotation.y += 0.01f;
    mesh.rotation.x -= 0.01f;
    //mesh.rotation.z += 0.01f;

    //mesh.translation.x += 0.004f;
    mesh.translation.z = 5.0f;

    //mesh.scale.x += 0.002f;
    //mesh.scale.y += 0.002f;

    // create scale matrix that will be used
    mat4_t scale_matrix = mat4_make_scale(mesh.scale.x, mesh.scale.y, mesh.scale.z);

    // create translation matrix
    mat4_t translation_matrix = mat4_make_translation(mesh.translation.x, mesh.translation.y, mesh.translation.z);

    // create rotation matrices
    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);

    // create world matrix
    mat4_t world_matrix = mat4_identity();
    world_matrix = mat4_mul_mat4(&scale_matrix, &world_matrix);
    world_matrix = mat4_mul_mat4(&rotation_matrix_x, &world_matrix);
    world_matrix = mat4_mul_mat4(&rotation_matrix_y, &world_matrix);
    world_matrix = mat4_mul_mat4(&rotation_matrix_z, &world_matrix);
    world_matrix = mat4_mul_mat4(&translation_matrix, &world_matrix);


    triangles_to_render = NULL;

    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 - 1];
        face_vertices[1] = mesh.vertices[mesh_face.b - 1];
        face_vertices[2] = mesh.vertices[mesh_face.c - 1];

        vec4_t transformed_vertices[3];

        for (int j = 0; j < 3; j++) {
            vec4_t transformed_vertex = vec4_from_vec3(face_vertices[j]);

            // use matrix to scale original matrix
            transformed_vertex = mat4_multiply_vec4(world_matrix, transformed_vertex);

            // save translated 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_subtract(vector_b, vector_a);
        vec3_t vector_ac = vec3_subtract(vector_c, vector_a);
        vec3_normalize(&vector_ab);
        vec3_normalize(&vector_ac);

        // compute the face normal using the cross product to find perpindicular
        vec3_t normal = vec3_cross(vector_ab, vector_ac);
        vec3_normalize(&normal);

        if (cull_method == CULL_BACKFACE) {
            // check backface culling

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

            // calculate how aligned the camera ray is with the face normal
            float dot_normal_camera = vec3_dot(normal, camera_ray);

            if (dot_normal_camera < 0) {
                continue;
            }
        }

        vec4_t projected_points[3];

        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 y 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);

            //projected_triangle.points[j] = projected_point;
        }

        // calculate color using normal and light direction, then invert
        float dot_color_p = -vec3_dot(normal, light.direction);

        color_t flat_shaded_color = light_apply_intensity(0xFFFFFFFF, dot_color_p);

        // calculate the average depth of the triangle
        float avg_depth = (transformed_vertices[0].z + transformed_vertices[1].z + transformed_vertices[2].z) / 3.0f;
#define FUNC3 roundf
        triangle_t projected_triangle = {
            .points = {
                { FUNC3(projected_points[0].x), FUNC3(projected_points[0].y) },
                { FUNC3(projected_points[1].x), FUNC3(projected_points[1].y) },
                { FUNC3(projected_points[2].x), FUNC3(projected_points[2].y) },
            },
            .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 },
            },
            .color = flat_shaded_color,
            .average_depth = avg_depth,
        };

        //vec2_t midpoint = triangle_midpoint(projected_triangle);
        //draw_rect(midpoint.x, midpoint.y, 10, 10, 0xFFFF0000);

        // save the projected triangle in the array of triangles
        array_push(triangles_to_render, projected_triangle);

        triangle_t normal_triangle;

    }
}

int depth_comp(const void* a, const void* b) {
    triangle_t arg_a = *(const triangle_t*)a;
    triangle_t arg_b = *(const triangle_t*)b;

    if (arg_a.average_depth > arg_b.average_depth) return -1;
    if (arg_a.average_depth < arg_b.average_depth) return 1;
    return 0;
}

void render(void) {
    draw_grid();
    //uint32_t colors[] = { 0xFFFF0000, 0xFF00FF00, 0xFF0000FF, 0xFF757500, 0xFF007575, 0xFF750075 };
    // Loop all projected points and render them
    size_t arr_length = array_length(triangles_to_render);

    // sort triangles
    if (sort_faces) qsort(triangles_to_render, arr_length, sizeof(triangle_t), depth_comp);

    for (int i = 0; i < arr_length; i++) {
        triangle_t triangle = triangles_to_render[i];

        switch (render_method) {
            case RENDER_WIRE:
                draw_triangle(triangle, 0x0000ff);
                break;
            case RENDER_WIRE_VERTEX:
                draw_triangle(triangle, 0x0000ff);
                draw_rect(triangle.points[0].x, triangle.points[0].y, 3, 3, 0xFFFFFF00);
                draw_rect(triangle.points[1].x, triangle.points[1].y, 3, 3, 0xFFFFFF00);
                draw_rect(triangle.points[2].x, triangle.points[2].y, 3, 3, 0xFFFFFF00);
                break;
            case RENDER_FILL_TRIANGLE:
                draw_filled_triangle(triangle);
                break;
            case RENDER_FILL_TRIANGLE_LINE:
                draw_filled_triangle(triangle);
                draw_triangle(triangle, 0xff0000ff);
                break;
            case RENDER_TEXTURED:
                draw_textured_triangle(triangle, mesh_texture);
                break;
            case RENDER_TEXTURED_LINE:
                draw_textured_triangle(triangle, mesh_texture);
                draw_triangle(triangle, 0xff0000ff);
            default:
                break;
        }

    }

    //vec2_t m = triangle_midpoint(t);
    //draw_rect(m.x, m.y, 10, 10, 0xFFFF0000);

    //draw_line(10, 10, 500, 500, 0x00FF00);
    array_free(triangles_to_render);

    render_color_buffer();

    clear_color_buffer(0xFF000000);

    SDL_RenderPresent(renderer);
}

void free_resources(void) {
    free(color_buffer);
    array_free(mesh.faces);
    array_free(mesh.vertices);
}

int main(int argc, char *argv[]) {
    is_running = initialize_window();

    setup();

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

    destroy_window();
    free_resources();

    return 0;
}
