/*
 * 3D Engine 
 * main.c: 
 * Based on pikuma.com 3D software renderer in C
 * Copyright (c) 2021 986-Studio. All rights reserved.
 *
 * Created by Manoël Trapier on 01/03/2021.
 */

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <libc.h>

#include <SDL.h>

#include <log.h>

#include <3dengine.h>
#include <array.h>
#include <matrix.h>
#include <triangle.h>
#include <display.h>
#include <vector.h>
#include <mesh.h>
#include <light.h>
#include <camera.h>
#include <clipping.h>

bool isRunning = true;

int previousFrameTime = 0;
double deltaTime = 0;

typedef enum displayMode_t
{
    DISPLAY_MODE_WIREFRAME,
    DISPLAY_MODE_SOLID,
    DISPLAY_MODE_SOLID_WIRE,
    DISPLAY_MODE_TEXTURED,
    DISPLAY_MODE_TEXTURED_WIRE,
} displayMode_t;

bool displayVertex = false;
bool doNotCull = false;
bool doZBuffer = true;
bool doClipping = true;
bool doPerspectiveCorrection = true;

static mesh_t mesh;
static int windowHeight;
static int windowWidth;

static enum displayMode_t currentDisplayMode = DISPLAY_MODE_WIREFRAME;
static matrix4_t projectionMatrix;
static matrix4_t viewMatrix;
static light_t globalLight;
static double updateTime, renderTime, waitTime;
static int trianglesCount, trianglesTotal;
static triangle_t *projectedTriangles = NULL;
static const vec3_t origin = {0, 0, 0 };

void setup()
{
    double FOV = 60;
    double aspectRatioY = (double)windowHeight / windowWidth;
    double aspectRatioX = (double)windowWidth / windowHeight;
    double zNear = 1;
    double zFar = 20;

    projectionMatrix = mat4Perspective(aspectRatioY, FOV, zNear, zFar);

    initFrustumPlanes(FOV, aspectRatioX, zNear, zFar);

    globalLight.direction.x = 0;
    globalLight.direction.y = 0;
    globalLight.direction.z = 1;
    vec3Normalize(&globalLight.direction);

    /* Load object */
    loadOBJFile(&mesh,"assets/dragon.obj");
    //loadCubeMeshData(&mesh);

    /* Load texture */
    loadTextureDateFromPng(&mesh.texture, "assets/f117.png");

    trianglesTotal = arrayGetSize(mesh.faces);
}

void processInput()
{
    SDL_Event event;
    while(SDL_PollEvent(&event))
    {
        switch (event.type)
        {
        case SDL_QUIT:isRunning = false;
            break;

        case SDL_KEYDOWN:
            switch (event.key.keysym.sym)
            {
            case SDLK_1:
                displayVertex = true;
                currentDisplayMode = DISPLAY_MODE_WIREFRAME;
                break;

            case SDLK_2:
                displayVertex = false;
                currentDisplayMode = DISPLAY_MODE_WIREFRAME;
                break;

            case SDLK_3:
                displayVertex = false;
                currentDisplayMode = DISPLAY_MODE_SOLID;
                break;

            case SDLK_4:
                displayVertex = false;
                currentDisplayMode = DISPLAY_MODE_SOLID_WIRE;
                break;

            case SDLK_5:
                displayVertex = false;
                currentDisplayMode = DISPLAY_MODE_TEXTURED;
                break;

            case SDLK_6:
                displayVertex = false;
                currentDisplayMode = DISPLAY_MODE_TEXTURED_WIRE;
                break;

            case SDLK_ESCAPE: isRunning = false; break;
            case SDLK_u: doNotCull = false; break;
            case SDLK_j: doNotCull = true; break;
            case SDLK_k: doPerspectiveCorrection = false; break;
            case SDLK_i: doPerspectiveCorrection = true; break;
            case SDLK_o: doZBuffer = true; break;
            case SDLK_l: doZBuffer = false; break;
            case SDLK_y: doClipping = true; break;
            case SDLK_h: doClipping = false; break;

            case SDLK_w:
                camera.forwardVelocity = vec3ScalarMult(camera.direction, 5 * deltaTime);
                camera.position = vec3AddVectors(camera.position, camera.forwardVelocity);
                break;

            case SDLK_d: camera.yawAngle += 1.0 * deltaTime; break;
            case SDLK_s:
                camera.forwardVelocity = vec3ScalarMult(camera.direction, 5 * deltaTime);
                camera.position = vec3SubVectors(camera.position, camera.forwardVelocity);
                break;

            case SDLK_a: camera.yawAngle -= 1.0 * deltaTime; break;
            case SDLK_UP: camera.position.y += 3.0 * deltaTime; break;
            case SDLK_DOWN: camera.position.y -= 3.0 * deltaTime; break;
            }
            break;

        default:
            break;
        }
    }
}

uint32_t getMicroSecTime()
{
    struct timeval curTime;
    gettimeofday(&curTime, NULL);
    return ((curTime.tv_sec) * 1000 * 1000) + (curTime.tv_usec);
}

static void projectTriangle(triangle_t *workTriangle, vec3_t *triangleNormal, texture_t *texture, colour_t colour)
{
    int j;
    struct triangle_t currentTriangle = *workTriangle;

    for (j = 0 ; j < 3 ; j++)
    {
        vec4_t projected = mat4ProdVec4Project(projectionMatrix, workTriangle->points[j]);

        projected.x *= windowWidth / 2.;
        projected.y *= windowHeight / 2.;

        /* Screen Y axis increase down, 3D world Y grows up, so we need to invert. */
        projected.y *= -1.0;

        projected.x += (windowWidth / 2.);
        projected.y += (windowHeight / 2.);

        currentTriangle.points[j].x = projected.x;
        currentTriangle.points[j].y = projected.y;
        currentTriangle.points[j].w = projected.w;
        currentTriangle.points[j].z = projected.z;
    }

    if (doNotCull)
    {
        currentTriangle.colour = colour;
    }
    else
    {
        double lightLevel = -vec3Dot(*triangleNormal, globalLight.direction);
        currentTriangle.colour = applyLight(colour, lightLevel);
    }
    currentTriangle.averageDepth = (workTriangle->points[0].z +
                                    workTriangle->points[1].z +
                                    workTriangle->points[2].z) / 3.0;

    currentTriangle.texture = texture;
    arrayAdd(projectedTriangles, currentTriangle);
}

void update()
{
    uint32_t i, j;
    uint32_t faceCount;
    uint32_t numberOfClippedTriangles;
    uint32_t timeToWait = FRAME_TARGET_TIME - (SDL_GetTicks() - previousFrameTime);
    matrix4_t worldMatrix;
    uint32_t ticks = getMicroSecTime();
    triangle_t trianglesAfterClipping[MAX_NUM_POLYGON_TRIANGLES];

    if (timeToWait <= FRAME_TARGET_TIME)
    {
        SDL_Delay(timeToWait);
    }
    deltaTime = (SDL_GetTicks() - previousFrameTime) / 1000.0;
    previousFrameTime = SDL_GetTicks();

    waitTime = (waitTime + (getMicroSecTime() - ticks) / 1000.) / 2.0;
    ticks = getMicroSecTime();

    mesh.translation.z = 5;

    vec3_t upDirection = { 0, 1, 0 };
    vec3_t lookAtTarget = { 0, 0, 1 };
    camera.direction = vec3FromVec4(mat4ProdVec4(mat4RotationY(camera.yawAngle), vec4FromVec3(lookAtTarget)));
    lookAtTarget = vec3AddVectors(camera.position, camera.direction);

    viewMatrix = mat4LookAt(camera.position, lookAtTarget, upDirection);

    worldMatrix = mat4Identity;
    worldMatrix = mat4ProdMat4(mat4Scale(mesh.scale.x, mesh.scale.y, mesh.scale.z), worldMatrix);
    worldMatrix = mat4ProdMat4(mat4RotationZ(mesh.rotation.z), worldMatrix);
    worldMatrix = mat4ProdMat4(mat4RotationY(mesh.rotation.y), worldMatrix);
    worldMatrix = mat4ProdMat4(mat4RotationX(mesh.rotation.x), worldMatrix);
    worldMatrix = mat4ProdMat4(mat4Translate(mesh.translation.x, mesh.translation.y, mesh.translation.z), worldMatrix);
    /* Last apply the view matrix */
    worldMatrix = mat4ProdMat4(viewMatrix, worldMatrix);

    arrayEmpty(projectedTriangles);

    faceCount = arrayGetSize(mesh.faces);
    for(i = 0; i < faceCount; i++)
    {
        polygon_t polygon;
        vec4_t transformedVertices[3];
        vec3_t triangleNormal;

        vec3_t vertices[3] =
        {
            mesh.vertices[mesh.faces[i].a],
            mesh.vertices[mesh.faces[i].b],
            mesh.vertices[mesh.faces[i].c],
        };

        for(j = 0; j < 3; j++)
        {
            transformedVertices[j] = vec4FromVec3(vertices[j]);
            transformedVertices[j] = mat4ProdVec4(worldMatrix, transformedVertices[j]);
        }

        vec3_t vectorBA = vec3SubVectors(vec3FromVec4(transformedVertices[1]), vec3FromVec4(transformedVertices[0]));
        vec3_t vectorCA = vec3SubVectors(vec3FromVec4(transformedVertices[2]), vec3FromVec4(transformedVertices[0]));
        vec3Normalize(&vectorBA);
        vec3Normalize(&vectorCA);
        triangleNormal = vec3Cross(vectorBA, vectorCA);
        vec3Normalize(&triangleNormal);

        if (!doNotCull)
        {
            vec3_t cameraRay = vec3SubVectors(origin, vec3FromVec4(transformedVertices[0]));
            vec3Normalize(&cameraRay);

            double cameraDotTriangle = vec3Dot(triangleNormal, cameraRay);

            if (cameraDotTriangle < 0)
            {
                continue;
            }
        }

        if (doClipping)
        {
            polygon = createPolygonFromTriangle(vec3FromVec4(transformedVertices[0]),
                                                vec3FromVec4(transformedVertices[1]),
                                                vec3FromVec4(transformedVertices[2]),
                                                mesh.faces[i].a_uv,
                                                mesh.faces[i].b_uv,
                                                mesh.faces[i].c_uv);

            clipPolygon(&polygon);
            createTriangleFromPolygon(&polygon, trianglesAfterClipping, &numberOfClippedTriangles);
            for(j = 0; j < numberOfClippedTriangles; j++)
            {
                projectTriangle(&trianglesAfterClipping[j], &triangleNormal, &mesh.texture, mesh.faces[i].colour);
            }
        }
        else
        {
            triangle_t workTriangle =
            {
                .points =
                {
                    [0] = transformedVertices[0],
                    [1] = transformedVertices[1],
                    [2] = transformedVertices[2]
                },
                .textureCoordinates =
                {
                    [0] = { mesh.faces[i].a_uv.u, mesh.faces[i].a_uv.v },
                    [1] = { mesh.faces[i].b_uv.u, mesh.faces[i].b_uv.v },
                    [2] = { mesh.faces[i].c_uv.u, mesh.faces[i].c_uv.v }
                }
            };

            projectTriangle(&workTriangle, &triangleNormal, &mesh.texture, mesh.faces[i].colour);
        };
    }

    if (!doZBuffer)
    {
        trianglesCount = arrayGetSize(projectedTriangles);

        qsort(projectedTriangles, trianglesCount, sizeof(triangle_t), compareTrianglesZOrder);
    }

    updateTime = (updateTime + (getMicroSecTime() - ticks) / 1000.) / 2.0;
}

void render()
{
    int i;
    uint32_t ticks = getMicroSecTime();

    clearFrameBuffer(MAKE_RGB(0, 0, 0));
    clearZBuffer();

    drawGrid(20, MAKE_RGB(27, 2, 27));

    int triangleCount = arrayGetSize(projectedTriangles);

    for(i = 0; i < triangleCount; i++)
    {
        switch(currentDisplayMode)
        {
        case DISPLAY_MODE_SOLID:
        case DISPLAY_MODE_SOLID_WIRE:
            drawFilledTriangle(&projectedTriangles[i]);
            break;

        case DISPLAY_MODE_TEXTURED:
        case DISPLAY_MODE_TEXTURED_WIRE:
            drawTextureTriangle(&projectedTriangles[i]);
            break;

        default:
            break;
        }

        switch (currentDisplayMode)
        {
        case DISPLAY_MODE_SOLID_WIRE:
        case DISPLAY_MODE_TEXTURED_WIRE:
        case DISPLAY_MODE_WIREFRAME:
            drawTriangle(projectedTriangles[i].points[0].x, projectedTriangles[i].points[0].y,
                         projectedTriangles[i].points[1].x, projectedTriangles[i].points[1].y,
                         projectedTriangles[i].points[2].x, projectedTriangles[i].points[2].y,
                         projectedTriangles[i].colour);
            break;
        default:
            break;
        }

        if (displayVertex)
        {
            int j;
            for(j = 0; j < 3; j++)
            {
                drawRectangle(projectedTriangles[i].points[j].x - 1,
                              projectedTriangles[i].points[j].y - 1,
                              3, 3,
                              MAKE_RGB(255, 0, 0));
            }
        }
    }

    renderFrameBuffer();

    renderTime = (renderTime + (getMicroSecTime() - ticks) / 1000.) / 2.0;

    drawText(5, 5, 0x11FF22, "Wait time: %02.2f ms", waitTime);
    drawText(5, 17, 0x11FF22, "Update time: %02.2f ms", updateTime);
    drawText(5, 29, 0x11FF22, "Render time: %02.2f ms", renderTime);
    drawText(5, 41, 0x11FF22, "FPS: %.1f | dT: %.3f", 1000 / (waitTime + updateTime + renderTime), deltaTime);
    drawText(5, 53, 0x11FF22, "Triangles: %d / %d", triangleCount, trianglesTotal);
    drawText(5, 65, 0x11FF22, "Cull: %c | PeCo: %c | ZB: %c, | CP: %c",
             doNotCull?'N':'Y', doPerspectiveCorrection ? 'Y' : 'N', doZBuffer ? 'Y' : 'N', doClipping ? 'Y' : 'N');

    displayWindowRender();
}

int main(int argc, char *argv[])
{
    int param_i;
    bool fullScreen = false;
    MAX_DEBUG_LEVEL = TLOG_DEBUG;

    Log(TLOG_ALWAYS, NULL, "Booting 3D Engine (version %s)!", VERSION);

    windowWidth = 320;
    windowHeight = 240;

    for (param_i = 1 ; (param_i < argc) && (argv[param_i][0] == '-') ; param_i++)
    {
        switch (argv[param_i][1])
        {
        default: exit(-1); /* Option not recognized */
        case 'f': fullScreen = true; break;
        case 'w': windowWidth = atoi(argv[++param_i]); break;
        case 'h': windowHeight = atoi(argv[++param_i]); break;
#ifdef DYNA_LOG_LEVEL
        case 'l': MAX_DEBUG_LEVEL = atoi(argv[++param_i]); break;
#endif
        case '-': goto no_more_params; /* Could use break, but this is more clear */
        }
    }
no_more_params:

    isRunning = initialiseWindow(windowWidth, windowHeight, fullScreen);

    setup();

    while (isRunning)
    {
        processInput();
        update();
        render();
    }

    arrayFree(projectedTriangles);
    textureCleanup(&mesh.texture);
    meshFree(&mesh);
    destroyWindow();

    return 0;
}