#include <limits.h>
#include "gameloop.h"
#include "textures.h"
#include "utility.h"
#include "options.h"
#include "blocks.h"
#include "menus.h"
#include "data.h"
#include "gui.h"


/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 * WARNING!!!                                                                *
 *                                                                           *
 * This file is where all the decompiled nonsense is. Things are so heavily  *
 * nested and spaghetti-like that its the only place with an indent size of  *
 * two spaces (predominantly).                                               *
 *                                                                           *
 * Looking at this code for extended periods of time can cause adverse       *
 * psychological effects.                                                    *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */


World world = {0};
Player *player = &world.player;

int gameState = STATE_TITLE;
int gamePopup;

static int guiOn;
static int debugOn;

static SDL_Rect backgroundRect;
static char *errorMessage = NULL;
static long l;

static void gameLoop_gameplay(SDL_Renderer *, Inputs *);
static void gameLoop_drawPopup(SDL_Renderer *, Inputs *);
static void gameLoop_processMovement(Inputs *, int);

static uint32_t
    fps_lastmil = 0,
    fps_count = 0,
    fps_now = 0;


/* gameLoop_resetGame
 * Resets elements of the game such as time and the player position. This will
 * also reset the world.
 */
void gameLoop_resetGame()
{
    l = SDL_GetTicks();

    gamePopup = 0;

    guiOn = 1;
    debugOn = 0;

    backgroundRect.x = 0;
    backgroundRect.y = 0;
    backgroundRect.w = BUFFER_W;
    backgroundRect.h = BUFFER_H;

    chatAdd("Game started");
}


/* gameLoop
 * Does all the raycasting stuff, moves the player around, etc.
 * If by chance the game ends, it returns false - which should
 * terminate the main while loop and end the program.
 */
int gameLoop(
    Inputs *inputs,
    SDL_Renderer *renderer)
{
    // If there is an error, show it and stop
    if (errorMessage)
    {
        if (state_err(renderer, inputs, errorMessage))
        {
            errorMessage = NULL;
            // TODO: add capability to recover from error:
            return 0;
        }
        else
        {
            return 1;
        }
    }

    switch (gameState)
    {
    case STATE_TITLE:
        // A main menu
        if (state_title(renderer, inputs, &gameState))
            return 0;
        break;

    case STATE_SELECT_WORLD:
        // World creation menu
        state_selectWorld(renderer, inputs, &gameState, &world);
        break;

    case STATE_NEW_WORLD:
        // World creation menu
        state_newWorld(renderer, inputs, &gameState, &world);
        break;

    case STATE_LOADING:
        // Generate a world and present a loading screen
        if (state_loading(renderer, &world, world.seed, player->pos))
        {
            gameLoop_resetGame();
            gameState = 5;
        };
        break;

    case STATE_GAMEPLAY:
        // The actual gameplay
        gameLoop_gameplay(renderer, inputs);
        break;

    case STATE_OPTIONS:
        state_options(renderer, inputs, &gameState);
        break;

    default:
        state_egg(renderer, inputs, &gameState);
        break;
    }

    if (gameState != STATE_GAMEPLAY || gamePopup != POPUP_HUD)
    {
        inputs->mouse.left = 0;
        inputs->mouse.right = 0;
    }

    return 1;
}


/* gameLoop_gameplay
 * Runs game logic and rendering. This is where most of the obfuscated code is.
 */
static void gameLoop_gameplay(SDL_Renderer *renderer, Inputs *inputs)
{
    static double
        f21,
        f22,
        f23,
        f24,
        f25,
        f26,
        f27,
        f28,
        f29,
        f30,
        f31,
        f32,
        f33,
        f34,
        f35,
        f36;

    static int
        blockSelected = 0,
        selectedPass;

    static IntCoords blockSelect = {0};
    static IntCoords blockSelectOffset = {0};
    static IntCoords coordPass = {0};
    static IntCoords blockRayPosition = {0};

    static Chunk *chunk;

    /* Look to see if there are chunks that need to be
    loaded in*/

    // static int chunkLoadNum = 0;
    // if(chunkLoadNum < CHUNKARR_SIZE) {
    // static IntCoords chunkLoadCoords = { 0 };
    // chunkLoadCoords.x =
    // ((chunkLoadNum % CHUNKARR_DIAM) -
    // CHUNKARR_RAD) * 64 + player.pos.x;
    // chunkLoadCoords.y =
    // (((chunkLoadNum / CHUNKARR_DIAM) % CHUNKARR_DIAM) -
    // CHUNKARR_RAD) * 64 + player.pos.y;
    // chunkLoadCoords.z =
    // ((chunkLoadNum / (CHUNKARR_DIAM * CHUNKARR_DIAM)) -
    // CHUNKARR_RAD) * 64 + player.pos.z;
    // chunkLoadNum++;
    //
    // genChunk (
    // &world, world.seed,
    // chunkLoadCoords.x,
    // chunkLoadCoords.y,
    // chunkLoadCoords.z, world->type, 0,
    // player.pos
    // );
    // } else {
    // chunkLoadNum = 0;
    // }

    ;
    int headInWater = World_getBlock(&world,
                                     player->pos.x,
                                     player->pos.y,
                                     player->pos.z) == BLOCK_WATER;

    int feetInWater = World_getBlock(&world,
                                     player->pos.x,
                                     player->pos.y + 1,
                                     player->pos.z) == BLOCK_WATER;

    int effectDrawDistance = options.drawDistance;
    // Restrict view distance while in water
    if (headInWater)
    {
        effectDrawDistance = 10;
    }

    // Update directional vectors
    player->vectorH.x = sin(player->hRot);
    player->vectorH.y = cos(player->hRot);
    player->vectorV.x = sin(player->vRot);
    player->vectorV.y = cos(player->vRot);

    // Skybox, basically
    double timeCoef;
    switch (world.dayNightMode)
    {
    case 0:
        timeCoef = (double)(world.time % 102944) / 16384;
        timeCoef = sin(timeCoef);
        timeCoef /= sqrt(timeCoef * timeCoef + (1.0 / 128.0));
        timeCoef = (timeCoef + 1) / 2;
        break;
    case 1:
        timeCoef = 1;
        break;
    case 2:
        timeCoef = 0;
        break;
    }

    // Change ambient color depending on if we are in the water or the air
    if (headInWater)
    {
        SDL_SetRenderDrawColor(
            renderer,
            48 * timeCoef,
            96 * timeCoef,
            200 * timeCoef,
            255);
    }
    else
    {
        SDL_SetRenderDrawColor(
            renderer,
            153 * timeCoef,
            204 * timeCoef,
            255 * timeCoef,
            255);
    }

    SDL_RenderClear(renderer);

    if (inputs->keyboard.esc)
    {
        gamePopup = gamePopup ? 0 : 1;
        inputs->keyboard.esc = 0;
    }

    fps_count++;
    if (fps_lastmil < SDL_GetTicks() - 1000)
    {
        fps_lastmil = SDL_GetTicks();
        fps_now = fps_count;
        fps_count = 0;
    }

    /* Things that should run at a constant speed, regardless
    of CPU power. If the rendering takes a long time, this
    will fire more times to compensate. */
    while (SDL_GetTicks() - l > 10L)
    {
        world.time++;
        l += 10L;
        gameLoop_processMovement(inputs, feetInWater);
    }

    if (gamePopup == POPUP_HUD)
    {
        if (blockSelected)
        {
            InvSlot *activeSlot = &player->inventory.hotbar[player->inventory.hotbarSelect];

            // Breaking blocks
            if (inputs->mouse.left > 0)
            {
                Block blockid = World_getBlock(
                    &world,
                    blockSelect.x,
                    blockSelect.y,
                    blockSelect.z);

                // Can't break other players
                if (blockid != BLOCK_PLAYER_BODY && blockid != BLOCK_PLAYER_HEAD)
                {
                    InvSlot pickedUp = {
                        .blockid = blockid,
                        .amount = 1,
                        .durability = 1};

                    Inventory_transferIn(&player->inventory, &pickedUp);

                    World_setBlock(
                        &world,
                        blockSelect.x,
                        blockSelect.y,
                        blockSelect.z, 0, 1);
                }
            }

            blockSelectOffset.x += blockSelect.x;
            blockSelectOffset.y += blockSelect.y;
            blockSelectOffset.z += blockSelect.z;

            // Placing blocks
            if (inputs->mouse.right > 0)
            {
                if (
                    // Player cannot be obstructing the block
                    (
                        fabs(player->pos.x - 0.5 - blockSelectOffset.x) >= 0.8 ||
                        fabs(player->pos.y - blockSelectOffset.y) >= 1.45 ||
                        fabs(player->pos.z - 0.5 - blockSelectOffset.z) >= 0.8) &&
                    // Player must have enough of that block
                    activeSlot->amount > 0)
                {
                    int blockSet = World_setBlock(
                        &world,
                        blockSelectOffset.x,
                        blockSelectOffset.y,
                        blockSelectOffset.z,
                        activeSlot->blockid, 1);

                    if (blockSet)
                    {
                        activeSlot->amount--;
                        if (activeSlot->amount == 0)
                            activeSlot->blockid = 0;
                    }
                }
            }
        }

        inputs->mouse.left = 0;
        inputs->mouse.right = 0;

        // Toggle GUI
        if (inputs->keyboard.f1)
        {
            inputs->keyboard.f1 = 0;
            guiOn ^= 1;
        }

        // Toggle debug mode
        if (inputs->keyboard.f3)
        {
            inputs->keyboard.f3 = 0;
            debugOn = !debugOn;
        }

        // Toggle advanced debug menu
        #ifndef small
                if (inputs->keyboard.f4)
                {
                    inputs->keyboard.f4 = 0;
                    gamePopup = (gamePopup == POPUP_ADVANCED_DEBUG) ? 0 : 4;
                }
        #endif

        // Enter chat
        if (inputs->keyboard.t)
        {
            // reset text input
            inputs->keyboard.t = 0;
            inputs->keyTyped = 0;
            gamePopup = POPUP_CHAT;
        }

        // Enter inventory
        if (inputs->keyboard.e)
        {
            inputs->keyboard.e = 0;
            gamePopup = POPUP_INVENTORY;
        }

        // Swap hotbar selection with offhand
        if (inputs->keyboard.f)
        {
            inputs->keyboard.f = 0;
            InvSlot_swap(
                &player->inventory.hotbar[player->inventory.hotbarSelect],
                &player->inventory.offhand);
        }

        // Select hotbar slots with scroll wheel
        if (inputs->mouse.wheel != 0)
        {
            player->inventory.hotbarSelect -= inputs->mouse.wheel;
            player->inventory.hotbarSelect = nmod(
                player->inventory.hotbarSelect, 9);
            inputs->mouse.wheel = 0;
        }

        // Select hotbar slots with number keys
        if (inputs->keyboard.num1)
        {
            player->inventory.hotbarSelect = 0;
        }
        if (inputs->keyboard.num2)
        {
            player->inventory.hotbarSelect = 1;
        }
        if (inputs->keyboard.num3)
        {
            player->inventory.hotbarSelect = 2;
        }
        if (inputs->keyboard.num4)
        {
            player->inventory.hotbarSelect = 3;
        }
        if (inputs->keyboard.num5)
        {
            player->inventory.hotbarSelect = 4;
        }
        if (inputs->keyboard.num6)
        {
            player->inventory.hotbarSelect = 5;
        }
        if (inputs->keyboard.num7)
        {
            player->inventory.hotbarSelect = 6;
        }
        if (inputs->keyboard.num8)
        {
            player->inventory.hotbarSelect = 7;
        }
        if (inputs->keyboard.num9)
        {
            player->inventory.hotbarSelect = 8;
        }
    }

    /* Cast rays. selectedPass passes wether or not a block is
    selected to the blockSelected variable */

    // Decrease fov when in water
    double effectFov = options.fov;
    if (headInWater)
    {
        effectFov += 20;
    }

    selectedPass = 0;
    for (int pixelX = 0; pixelX < BUFFER_W; pixelX++)
    {
        double rayOffsetX = (pixelX - BUFFER_HALF_W) / effectFov;
        for (int pixelY = 0; pixelY < BUFFER_H; pixelY++)
        {
            int finalPixelColor = 0;
            int pixelMist = 255;
            int pixelShade;

            double rayOffsetY = (pixelY - BUFFER_HALF_H) / effectFov;

            // Ray offset Z?
            f21 = 1.0;

            f22 = f21 * player->vectorV.y + rayOffsetY * player->vectorV.x;
            f23 = rayOffsetY * player->vectorV.y - f21 * player->vectorV.x;
            f24 = rayOffsetX * player->vectorH.y + f22 * player->vectorH.x;
            f25 = f22 * player->vectorH.y - rayOffsetX * player->vectorH.x;

            double rayDistanceLimit = effectDrawDistance;

            f26 = 5.0;
            for (int blockFace = 0; blockFace < 3; blockFace++)
            {
                f27 = f24;
                if (blockFace == 1)
                    f27 = f23;
                if (blockFace == 2)
                    f27 = f25;
                f28 = 1.0 / ((f27 < 0.0) ? (-1 * f27) : f27);
                f29 = f24 * f28;
                f30 = f23 * f28;
                f31 = f25 * f28;
                f32 = player->pos.x - floor(player->pos.x);
                if (blockFace == 1)
                    f32 = player->pos.y - floor(player->pos.y);
                if (blockFace == 2)
                    f32 = player->pos.z - floor(player->pos.z);
                if (f27 > 0.0)
                    f32 = 1.0 - f32;
                f33 = f28 * f32;
                f34 = player->pos.x + f29 * f32;
                f35 = player->pos.y + f30 * f32;
                f36 = player->pos.z + f31 * f32;
                if (f27 < 0.0)
                {
                    if (blockFace == 0)
                        f34--;
                    if (blockFace == 1)
                        f35--;
                    if (blockFace == 2)
                        f36--;
                }

                /* Whatever's in this loop needs to run *extremely*
                fast */
                while (f33 < rayDistanceLimit)
                {
                    blockRayPosition.x = floor(f34);
                    blockRayPosition.y = floor(f35);
                    blockRayPosition.z = floor(f36);

                    /* Imitate getBlock so we don't have to launch
                    into a function then another function a zillion
                    times per second. This MUST BE STATIC because
                    this information needs to carry over between
                    iterations of gameLoop. */
                    // TODO: make this an inline function
                    static IntCoords lookup_ago = {
                                         100000000,
                                         100000000,
                                         100000000},
                                     lookup_now;

                    lookup_now.x = blockRayPosition.x >> 6;
                    lookup_now.y = blockRayPosition.y >> 6;
                    lookup_now.z = blockRayPosition.z >> 6;

                    if (
                        lookup_now.x != lookup_ago.x ||
                        lookup_now.y != lookup_ago.y ||
                        lookup_now.z != lookup_ago.z)
                    {
                        lookup_ago = lookup_now;

                        // hash coordinates
                        lookup_now.x &= 0x3FF;
                        lookup_now.y &= 0x3FF;
                        lookup_now.z &= 0x3FF;

                        lookup_now.y <<= 10;
                        lookup_now.z <<= 20;

                        uint32_t lookup_hash = lookup_now.x | lookup_now.y | lookup_now.z;
                        lookup_hash++;

                        int lookup_first = 0,
                            lookup_last = CHUNKARR_SIZE - 1,
                            lookup_middle = (CHUNKARR_SIZE - 1) / 2;

                        // Perform binary search
                        while (lookup_first <= lookup_last)
                        {
                            if (world.chunk[lookup_middle].coordHash > lookup_hash)
                            {
                                lookup_first = lookup_middle + 1;
                            }
                            else if (world.chunk[lookup_middle].coordHash == lookup_hash)
                            {
                                chunk = &world.chunk[lookup_middle];
                                goto foundChunk;
                            }
                            else
                            {
                                lookup_last = lookup_middle - 1;
                            }

                            lookup_middle = (lookup_first + lookup_last) / 2;
                        }
                        chunk = NULL;
                    }

                    Block intersectedBlock;
                foundChunk:
                    if (chunk)
                    {
                        intersectedBlock = chunk->blocks[nmod(blockRayPosition.x, 64) +
                                                         (nmod(blockRayPosition.y, 64) << 6) +
                                                         (nmod(blockRayPosition.z, 64) << 12)];
                    }
                    else
                    {
                        intersectedBlock = 0;
                        goto chunkNull;
                    }

                    if (
                        intersectedBlock != BLOCK_AIR &&
                        !(headInWater && intersectedBlock == BLOCK_WATER))
                    {
                        // Determine what texel the ray hit
                        int textureX = (int)floor((f34 + f36) * 16.0) & 0xF;
                        int textureY = ((int)floor(f35 * 16.0) & 0xF) + 16;
                        if (blockFace == 1)
                        {
                            textureX = (int)floor(f34 * 16.0) & 0xF;
                            textureY = (int)floor(f36 * 16.0) & 0xF;
                            if (f30 < 0.0)
                                textureY += 32;
                        }

                        // Block outline color
                        int pixelColor = 0xFFFFFF;
                        if (
                            (
                                !blockSelected ||
                                blockRayPosition.x != blockSelect.x ||
                                blockRayPosition.y != blockSelect.y ||
                                blockRayPosition.z != blockSelect.z) ||
                            (textureX > 0 && textureY % 16 > 0 && textureX < 15 && textureY % 16 < 15) || !guiOn || gamePopup)
                        {
                            if (intersectedBlock >= NUMBER_OF_BLOCKS)
                            {
                                pixelColor = 0xFF0000;
                            }
                            else
                            {
                                pixelColor = textures[textureX + (textureY * 16) + intersectedBlock * 256 * 3];
                            }
                        }

                        /* See if the block is selected. There must be a
                        better way to do this check... */
                        if (
                            f33 < f26 &&
                            (
                                (!options.trapMouse && pixelX == inputs->mouse.x / BUFFER_SCALE && pixelY == inputs->mouse.y / BUFFER_SCALE) ||
                                (options.trapMouse && pixelX == BUFFER_HALF_W && pixelY == BUFFER_HALF_H)
                                )
                            )
                        {
                            selectedPass = 1;
                            coordPass = blockRayPosition;

                            blockSelectOffset = (const IntCoords){0};
                            switch (blockFace)
                            {
                            case 0:
                                blockSelectOffset.x = 1 - 2 * (f27 > 0.0);
                                break;
                            case 1:
                                blockSelectOffset.y = 1 - 2 * (f27 > 0.0);
                                break;
                            case 2:
                                blockSelectOffset.z = 1 - 2 * (f27 > 0.0);
                                break;
                            }

                            f26 = f33;
                        }

                        if (pixelColor > 0)
                        {
                            finalPixelColor = pixelColor;
                            pixelMist = 255 - (int)(f33 / (double)effectDrawDistance * 255.0F);
                            pixelShade = 255 - (blockFace + 2) % 3 * 50;
                            rayDistanceLimit = f33;
                        }
                    }
                chunkNull:
                    f34 += f29;
                    f35 += f30;
                    f36 += f31;
                    f33 += f28;
                } // This concludes our warpspeed rampage
            }

            // Draw inverted color crosshair
            if (options.trapMouse && ((pixelX == BUFFER_HALF_W && abs(BUFFER_HALF_H - pixelY) < 4) ||
                                      (pixelY == BUFFER_HALF_H && abs(BUFFER_HALF_W - pixelX) < 4)))
            {
                finalPixelColor = 0x1000000 - finalPixelColor;
            }

            if (finalPixelColor > 0)
            {
                SDL_SetRenderDrawColor(
                    renderer,
                    ((finalPixelColor >> 16 & 0xFF) * pixelShade) >> 8,
                    ((finalPixelColor >> 8 & 0xFF) * pixelShade) >> 8,
                    ((finalPixelColor & 0xFF) * pixelShade) >> 8,
                    options.fogType ? sqrt(pixelMist) * 16 : pixelMist);

                SDL_RenderDrawPoint(renderer, pixelX, pixelY);
            }
        }
    }

    // Make camera blue if in water
    if (headInWater)
    {
        SDL_SetRenderDrawColor(renderer, 16, 32, 255, 128);
        SDL_RenderFillRect(renderer, &backgroundRect);
    }

    // Pass info about selected block on
    blockSelected = selectedPass;
    blockSelect = coordPass;

    inputs->mouse.x /= BUFFER_SCALE;
    inputs->mouse.y /= BUFFER_SCALE;

    // Draw current popup
    gameLoop_drawPopup(renderer, inputs);

    // If we need to take a screenshot, do so
    if (inputs->keyboard.f2)
    {
        inputs->keyboard.f2 = 0;
        char path[PATH_MAX];
        int err = data_getScreenshotPath(path);
        gameLoop_screenshot(renderer, err ? NULL : path);
    }
}


void gameLoop_drawPopup(SDL_Renderer *renderer, Inputs *inputs)
{
    // In-game menus
    if (gamePopup != 0)
    {
        SDL_SetRelativeMouseMode(0);
    }

    switch (gamePopup)
    {
    case POPUP_HUD:
        // HUD
        if (options.trapMouse)
            SDL_SetRelativeMouseMode(1);
        if (guiOn)
            popup_hud(
                renderer, inputs, &world,
                &debugOn, &fps_now, player);
        break;

    case POPUP_PAUSE:
        // Pause menu
        tblack(renderer);
        SDL_RenderFillRect(renderer, &backgroundRect);
        popup_pause(renderer, inputs, &gamePopup, &gameState, &world);
        break;

    case POPUP_OPTIONS:
        // Options
        tblack(renderer);
        SDL_RenderFillRect(renderer, &backgroundRect);
        popup_options(renderer, inputs, &gamePopup);
        break;

    case POPUP_INVENTORY:
        // Inventory
        popup_inventory(renderer, inputs, player, &gamePopup);
        break;

    #ifndef small
        case POPUP_ADVANCED_DEBUG:
            // Advanced debug menu
            tblack(renderer);
            SDL_RenderFillRect(renderer, &backgroundRect);
            popup_debugTools(renderer, inputs, &gamePopup);
            break;

        case POPUP_CHUNK_PEEK:
            // Chunk peek
            tblack(renderer);
            SDL_RenderFillRect(renderer, &backgroundRect);
            popup_chunkPeek(renderer, inputs, &world, &gamePopup, player);
            break;

        case POPUP_ROLL_CALL:
            // Chunk info viewer
            tblack(renderer);
            SDL_RenderFillRect(renderer, &backgroundRect);
            popup_rollCall(renderer, inputs, &world, &gamePopup);
            break;

        case POPUP_OVERVIEW:
            // View all chunks
            tblack(renderer);
            SDL_RenderFillRect(renderer, &backgroundRect);
            popup_overview(renderer, inputs, &world, &gamePopup);
            break;
    #endif

    case POPUP_CHAT:
        // Chat
        popup_chat(renderer, inputs, world.time);
        break;
    }
}


static void gameLoop_processMovement(Inputs *inputs, int inWater)
{
    // Run at half speed if in water
    static int flipFlop = 0;
    flipFlop = !flipFlop;
    int doPhysics = !inWater || flipFlop;

    // Only process movement controls if there are no active popup
    if (gamePopup == 0)
    {
        // Looking around
        if (options.trapMouse)
        {
            player->hRot += (double)inputs->mouse.x / 64;
            player->vRot -= (double)inputs->mouse.y / 64;
        }
        else
        {
            double cameraMoveX =
                (inputs->mouse.x - BUFFER_W * 2) /
                (double)BUFFER_W * 2.0;
            double cameraMoveY =
                (inputs->mouse.y - BUFFER_H * 2) /
                (double)BUFFER_H * 2.0;

            double cameraMoveDistance = sqrt(
                                            cameraMoveX * cameraMoveX +
                                            cameraMoveY * cameraMoveY) -
                                        1.2;

            if (cameraMoveDistance < 0.0)
            {
                cameraMoveDistance = 0.0;
            }
            if (cameraMoveDistance > 0.0)
            {
                player->hRot += cameraMoveX *
                                cameraMoveDistance / 400.0;
                player->vRot -= cameraMoveY *
                                cameraMoveDistance / 400.0;
            }
        }

        // Restrict camera vertical position
        if (player->vRot < -1.57)
            player->vRot = -1.57;
        if (player->vRot > 1.57)
            player->vRot = 1.57;

        double speed = 0.02;

        if (doPhysics)
        {
            player->FBVelocity =
                (inputs->keyboard.w - inputs->keyboard.s) *
                speed;
            player->LRVelocity =
                (inputs->keyboard.d - inputs->keyboard.a) *
                speed;
        }
    }

    static Coords playerMovement = {0.0, 0.0, 0.0};

    // Moving around
    if (doPhysics)
    {
        playerMovement.x *= 0.5;
        playerMovement.y *= 0.99;
        playerMovement.z *= 0.5;

        playerMovement.x +=
            player->vectorH.x * player->FBVelocity +
            player->vectorH.y * player->LRVelocity;
        playerMovement.z +=
            player->vectorH.y * player->FBVelocity -
            player->vectorH.x * player->LRVelocity;
        playerMovement.y += 0.003;
    }

    // Detect collisions and jump
    for (int axis = 0; axis < 3; axis++)
    {
        if (!doPhysics)
        {
            break;
        }

        Coords playerPosTry = {
            player->pos.x + playerMovement.x * (double)((axis + 2) % 3 / 2),
            player->pos.y + playerMovement.y * (double)((axis + 1) % 3 / 2),
            player->pos.z + playerMovement.z * (double)((axis + 3) % 3 / 2),
        };

        for (int step = 0; step < 12; step++)
        {
            int blockX = floor(playerPosTry.x +
                               ((step >> 0) & 1) * 0.6 - 0.3);
            int blockY = floor(playerPosTry.y +
                               ((step >> 2) - 1) * 0.8 + 0.65);
            int blockZ = floor(playerPosTry.z +
                               ((step >> 1) & 1) * 0.6 - 0.3);

            // Very ad-hoc. TODO: look into a deeper fix than this.
            // blockX -= (blockX < 0);
            // blockY -= (blockY < 0);
            // blockZ -= (blockZ < 0);
            // ---

            Block block = World_getBlock(&world,
                                         blockX,
                                         blockY,
                                         blockZ);

            int shouldCollide = 1;
            // Blocks that have collision disabled
            shouldCollide &= block != BLOCK_AIR;
            shouldCollide &= block != BLOCK_WATER;
            shouldCollide &= block != BLOCK_TALL_GRASS;
            // Uncomment this line to be able to enter chunks that
            // don't exist in memory
            // shouldCollide &= block != BLOCK_NIL;

            if (shouldCollide)
            {
                if (axis != 1)
                {
                    goto stopCheck;
                }

                if (
                    inputs->keyboard.space > 0 &&
                    (playerMovement.y > 0.0) &&
                    !gamePopup)
                {
                    inputs->keyboard.space = 0;
                    playerMovement.y = -0.1;
                    goto stopCheck;
                }
                playerMovement.y = 0.0;
                goto stopCheck;
            }
        }

        player->pos.x = playerPosTry.x;
        player->pos.y = playerPosTry.y;
        player->pos.z = playerPosTry.z;

    stopCheck:;
    }

    // Swim in water
    if (inWater && doPhysics)
    {
        if (
            inputs->keyboard.space > 0 &&
            (playerMovement.y > -0.05) &&
            !gamePopup)
        {
            inputs->keyboard.space = 0;
            playerMovement.y = -0.1;
        }
    }
}


int gameLoop_screenshot(SDL_Renderer *renderer, const char *path)
{
    SDL_Surface *grab = SDL_CreateRGBSurfaceWithFormat(
        0, BUFFER_W * BUFFER_SCALE, BUFFER_H * BUFFER_SCALE,
        32, SDL_PIXELFORMAT_ARGB8888);

    SDL_RenderReadPixels(
        renderer, NULL, SDL_PIXELFORMAT_ARGB8888,
        grab->pixels, grab->pitch);

    if (path == NULL)
    {
        chatAdd("Couldn't save screenshot");
        return 1;
    }

    int saved = SDL_SaveBMP(grab, path);
    SDL_FreeSurface(grab);

    if (saved == 0)
    {
        chatAdd("Saved screenshot");
        return 0;
    }
    else
    {
        chatAdd("Couldn't save screenshot");
        return 1;
    }
}


void gameLoop_error(char *message)
{
    errorMessage = message;
}
