/*
 * Voxel-a-tord
 * main.c:
 * Copyright (c) 2022 986-Studio. All rights reserved.
 *
 * Created by Manoël Trapier on 28/09/2022.
 */

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <sys/time.h>

#include <SDL.h>

#include <log.h>
#include <display.h>
#include <texture.h>
#include <fpmath.h>

bool isRunning = false;
int previousFrameTime = 0;
double deltaTime = 0;

static double updateTime, renderTime, waitTime;
static int32_t windowHeight;
static int32_t windowWidth;
static texture_t heightMap;
static texture_t colourMap;
static int32_t *yBuffer;

static int32_t playerHeight = 70;
static fp_num_t playerX = FP_SET_INT(444);
static fp_num_t playerY = FP_SET_INT(299);
static int32_t playerRot = 0;
static int32_t cameraHorizon = 55;
static int32_t scaleFactor = 90;
static int32_t renderDistance = 300;

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

void processInput()
{
    SDL_Event event;
    double direction = FP_GET_INT(1);
    fp_num_t dx, dy;

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

        case SDL_KEYDOWN:
            switch (event.key.keysym.sym)
            {
            case SDLK_ESCAPE: isRunning = false; break;

            case SDLK_LEFT: playerRot = playerRot + 1; break;
            case SDLK_RIGHT: playerRot = playerRot - 1; break;

            case SDLK_DOWN:
                direction = FP_GET_INT(-1);
                /* fall-trough */

            case SDLK_UP:
                dx = FP_MUL(FP_MUL(fpcos(playerRot), 75), FP_SET_FLT(deltaTime));
                dy = FP_MUL(FP_MUL(fpsin(playerRot), 75), FP_SET_FLT(deltaTime));
                playerX += FP_MUL(dx, direction);
                playerY += FP_MUL(dy, direction);
                break;

            case SDLK_KP_4: playerX -= FP_SET_INT(1); break;
            case SDLK_KP_6: playerX += FP_SET_INT(1); break;
            case SDLK_KP_8: playerY += FP_SET_INT(1); break;
            case SDLK_KP_2: playerY -= FP_SET_INT(1); break;

            case SDLK_PAGEUP: playerHeight = playerHeight + 5; break;
            case SDLK_PAGEDOWN: playerHeight = playerHeight - 5; break;

            case SDLK_HOME: cameraHorizon = cameraHorizon + 5; break;
            case SDLK_END: cameraHorizon = cameraHorizon - 5; break;

            case SDLK_KP_MULTIPLY: scaleFactor = scaleFactor + 3; break;
            case SDLK_KP_DIVIDE: scaleFactor = scaleFactor - 3; break;

            case SDLK_KP_PLUS: renderDistance = renderDistance + 3; break;
            case SDLK_KP_MINUS: renderDistance = renderDistance - 3; break;
            }
            break;

        default:
            break;
        }
    }
}

void setup()
{
    loadTextureDateFromPng(&heightMap, "assets/D1.png");
    loadTextureDateFromPng(&colourMap, "assets/C1W.png");

    yBuffer = (int32_t *)calloc(windowWidth, sizeof(int32_t));

    isRunning = true;
}

void update()
{
    uint32_t timeToWait = FRAME_TARGET_TIME - (SDL_GetTicks() - previousFrameTime);
    uint32_t ticks = getMicroSecTime();

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

    /* [...] */

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

void renderVoxels(tex2_t *p, int32_t angle, int32_t height, int32_t horizon, int32_t scaleHeight, int32_t renderDistance)
{
    fp_num_t angleSin = fpsin(angle);
    fp_num_t angleCos = fpcos(angle);
    fp_num_t dz = FP_SET_INT(1), z = FP_SET_INT(1);
    int i;

    for(i = 0; i < windowWidth; i++)
    {
        yBuffer[i] = FP_SET_INT(windowHeight);
    }

    while( FP_GET_INT(z) < renderDistance)
    {
        tex2_t pleft;
        pleft.u = -FP_MUL(angleCos, z) - FP_MUL(angleSin, z) + p->u;
        pleft.v = FP_MUL(angleSin, z) - FP_MUL(angleCos, z) + p->v;
        tex2_t pright;
        pright.u = FP_MUL(angleCos, z) - FP_MUL(angleSin, z) + p->u;
        pright.v = -FP_MUL(angleSin, z) - FP_MUL(angleCos, z) + p->v;

        fp_num_t dx = (pright.u - pleft.u);
        dx = FP_DIV(dx, FP_SET_INT(windowWidth));
        fp_num_t dy = (pright.v - pleft.v);
        dy = FP_DIV(dy, FP_SET_INT(windowWidth));

        for(i = 0; i < windowWidth; i++)
        {
            int32_t voxelHeight2 = getTextureColour(&heightMap, &pleft);
            voxelHeight2 = height - voxelHeight2;
            fp_num_t temp = FP_MUL(FP_DIV(FP_SET_INT(1), z), FP_SET_INT(scaleHeight));
            voxelHeight2 = FP_GET_INT(FP_MUL(temp, FP_SET_INT(voxelHeight2)));
            int32_t heightOnScreen = voxelHeight2 + horizon;

            if (heightOnScreen > 0 && heightOnScreen < yBuffer[i])
            {
                colour_t voxelColour = getTextureColour(&colourMap, &pleft);
                drawVLine(i, yBuffer[i], heightOnScreen, voxelColour);
                yBuffer[i] = heightOnScreen;
            }
            pleft.u += dx;
            pleft.v += dy;
        }

        z += dz;

#ifdef USE_FLOATING_POINT
        dz += 0.01;
#else
        dz += 1; // (fp_num_t)(0.1 * FP_DIVISOR);
#endif
    }
}

void render()
{
    uint32_t ticks = getMicroSecTime();
    tex2_t origin = { .u = playerX, .v = playerY };

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

    renderVoxels(& origin, playerRot, playerHeight, cameraHorizon, scaleFactor, renderDistance);

    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);
#ifdef USE_FLOATING_POINT
    drawText(5, 53, 0x11FF22, "P: (%.1f;%.1f) | PH: %.1f | CH: %.1f | SF: %.1f | RD: %.1f", playerX, playerY, playerHeight, cameraHorizon, scaleFactor, renderDistance);
#else
    drawText(5, 53, 0x11FF22, "P: (%d;%d) | PH: %d | CH: %d | SF: %d | RD: %d", FP_GET_INT(playerX),
             FP_GET_INT(playerY),
             playerHeight,
             cameraHorizon,
             scaleFactor,
             renderDistance);
#endif

    displayWindowRender();
}

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

    Log(TLOG_ALWAYS, NULL, "Starting Voxel Engine (version %s)!", VERSION);

    windowWidth = 800;
    windowHeight = 600;

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

    destroyWindow();

    return 0;
}