#include "app.h"
#include "chunk_mesh.h"
#include "debug_draw.h"
#include "texture_atlas.h"

#include <algorithm>
#include <array>
#include <cmath>
#include <cstring>
#include <stdexcept>

#include "third_party/stb_image.h"

void App::createCommandPool() {
    QueueFamilyIndices indices = findQueueFamilies(physicalDevice_);

    VkCommandPoolCreateInfo poolInfo{};
    poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
    poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
    poolInfo.queueFamilyIndex = indices.graphicsFamily.value();

    if (vkCreateCommandPool(device_, &poolInfo, nullptr, &commandPool_) != VK_SUCCESS) {
        throw std::runtime_error("vkCreateCommandPool failed");
    }
}
void App::createCommandBuffers() {
    commandBuffers_.resize(swapchainFramebuffers_.size());

    VkCommandBufferAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    allocInfo.commandPool = commandPool_;
    allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
    allocInfo.commandBufferCount = static_cast<uint32_t>(commandBuffers_.size());

    if (vkAllocateCommandBuffers(device_, &allocInfo, commandBuffers_.data()) != VK_SUCCESS) {
        throw std::runtime_error("vkAllocateCommandBuffers failed");
    }
}
void App::recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex) {
    VkCommandBufferBeginInfo beginInfo{};
    beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;

    if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) {
        throw std::runtime_error("vkBeginCommandBuffer failed");
    }

    std::array<VkClearValue, 2> clearValues{};
    clearValues[0].color = {{skyR_, skyG_, skyB_, 1.0f}};
    clearValues[1].depthStencil = {1.0f, 0};

    VkRenderPassBeginInfo renderPassInfo{};
    renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
    renderPassInfo.renderPass = renderPass_;
    renderPassInfo.framebuffer = swapchainFramebuffers_[imageIndex];
    renderPassInfo.renderArea.offset = {0, 0};
    renderPassInfo.renderArea.extent = swapchainExtent_;
    renderPassInfo.clearValueCount = static_cast<uint32_t>(clearValues.size());
    renderPassInfo.pClearValues = clearValues.data();

    vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);

    vkCmdBindDescriptorSets(
        commandBuffer,
        VK_PIPELINE_BIND_POINT_GRAPHICS,
        pipelineLayout_,
        0,
        1,
        &descriptorSets_[imageIndex],
        0,
        nullptr
    );

    // Use double-precision camera position to avoid float cancellation at large coords.
    const double camX = cameraX_;
    const double camY = cameraY_;
    const double camZ = cameraZ_;

    // Shared model for non-chunk world-space geometry (stars, debug, outline).
    auto makeCamRelModel = [&](float yAnimOff) -> std::array<float, 16> {
        return { 1.0f,0.0f,0.0f,0.0f,
                 0.0f,1.0f,0.0f,0.0f,
                 0.0f,0.0f,1.0f,0.0f,
                 static_cast<float>(-camX), yAnimOff + static_cast<float>(-camY), static_cast<float>(-camZ), 1.0f };
    };

    {
        auto m = makeCamRelModel(0.0f);
        vkCmdPushConstants(
            commandBuffer, pipelineLayout_,
            VK_SHADER_STAGE_VERTEX_BIT, 0, 64, m.data());
    }

    vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline_);

    // Build 6 frustum planes from the MVP matrix (Gribb–Hartmann method).
    // lastMvp_ is column-major: element [col*4+row].
    // Row i = { M[i], M[4+i], M[8+i], M[12+i] }.
    // Plane equation: dot(plane.xyz, worldPos) + plane.w >= 0 means inside.
    const float* M = lastMvp_;
    const float frustum[6][4] = {
        { M[0]+M[3],  M[4]+M[7],  M[8]+M[11],  M[12]+M[15] }, // left
        { M[3]-M[0],  M[7]-M[4],  M[11]-M[8],  M[15]-M[12] }, // right
        { M[1]+M[3],  M[5]+M[7],  M[9]+M[11],  M[13]+M[15] }, // bottom
        { M[3]-M[1],  M[7]-M[5],  M[11]-M[9],  M[15]-M[13] }, // top
        { M[2],       M[6],       M[10],        M[14]        }, // near (Vulkan depth [0,1])
        { M[3]-M[2],  M[7]-M[6],  M[11]-M[10], M[15]-M[14] }, // far
    };
    const float CS = static_cast<float>(Chunk::SIZE);

    // Returns false if the chunk AABB is fully outside any frustum plane.
    // Chunk AABB is expressed in camera-relative space (world - camPos),
    // computed in double for precision — lastMvp_ is now proj*viewRot (no translation).
    auto inFrustum = [&](const ChunkPos& pos) -> bool {
        const double dcs = static_cast<double>(Chunk::SIZE);
        const float x0 = static_cast<float>(static_cast<double>(pos.x) * dcs - static_cast<double>(camX));
        const float y0 = static_cast<float>(static_cast<double>(pos.y) * dcs - static_cast<double>(camY));
        const float z0 = static_cast<float>(static_cast<double>(pos.z) * dcs - static_cast<double>(camZ));
        const float x1 = x0 + CS, y1 = y0 + CS, z1 = z0 + CS;
        for (const auto& p : frustum) {
            const float px = (p[0] >= 0.0f) ? x1 : x0;
            const float py = (p[1] >= 0.0f) ? y1 : y0;
            const float pz = (p[2] >= 0.0f) ? z1 : z0;
            if (p[0]*px + p[1]*py + p[2]*pz + p[3] < 0.0f) return false;
        }
        return true;
    };

    // Chunk rise animation: new chunks slide in from 20 blocks "below" (along gravity) over 0.45 s.
    constexpr float ANIM_DUR    = 0.45f;
    constexpr float ANIM_DROP   = 20.0f; // blocks offset from final position

    // Returns animated offset magnitude for a chunk (0 once animation is done).
    // Negative = chunk is behind its final position (sliding toward zero).
    auto chunkAnimOff = [&](float spawnTime) -> float {
        const float age = currentTime_ - spawnTime;
        if (age >= ANIM_DUR) return 0.0f;
        const float t = age / ANIM_DUR;
        const float ease = t * t * (3.0f - 2.0f * t); // smoothstep
        return -ANIM_DROP * (1.0f - ease);
    };

    // Gravity-aware chunk model matrix: animation offset is along spawnUp direction
    auto makeChunkModelGrav = [&](const ChunkPos& cpos, float animOff, const Vec3& spUp)
        -> std::array<float, 16>
    {
        const double tx = static_cast<double>(cpos.x) * static_cast<double>(Chunk::SIZE) - camX
                        + static_cast<double>(animOff * spUp.x);
        const double ty = static_cast<double>(cpos.y) * static_cast<double>(Chunk::SIZE) - camY
                        + static_cast<double>(animOff * spUp.y);
        const double tz = static_cast<double>(cpos.z) * static_cast<double>(Chunk::SIZE) - camZ
                        + static_cast<double>(animOff * spUp.z);
        return { 1.0f,0.0f,0.0f,0.0f,
                 0.0f,1.0f,0.0f,0.0f,
                 0.0f,0.0f,1.0f,0.0f,
                 static_cast<float>(tx), static_cast<float>(ty), static_cast<float>(tz), 1.0f };
    };

    // 1. Спершу тверді блоки — per-chunk model matrix (local vertex coords + double precision)
    for (const auto& [cpos, mesh] : chunkMeshes_) {
        if (mesh.opaqueIndexCount == 0) continue;
        if (!inFrustum(cpos)) continue;

        const float animOff = chunkAnimOff(mesh.spawnTime);
        auto m = makeChunkModelGrav(cpos, animOff, mesh.spawnUp);
        vkCmdPushConstants(commandBuffer, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, 0, 64, m.data());

        VkBuffer vertexBuffers[] = { mesh.opaqueVertexBuffer };
        VkDeviceSize offsets[] = { 0 };
        vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets);
        vkCmdBindIndexBuffer(commandBuffer, mesh.opaqueIndexBuffer, 0, VK_INDEX_TYPE_UINT32);
        vkCmdDrawIndexed(commandBuffer, mesh.opaqueIndexCount, 1, 0, 0, 0);
    }

    // 2. Потім вода — same per-chunk model
    for (const auto& [cpos, mesh] : chunkMeshes_) {
        if (mesh.waterIndexCount == 0) continue;
        if (!inFrustum(cpos)) continue;

        const float animOff = chunkAnimOff(mesh.spawnTime);
        auto m = makeChunkModelGrav(cpos, animOff, mesh.spawnUp);
        vkCmdPushConstants(commandBuffer, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, 0, 64, m.data());

        VkBuffer vertexBuffers[] = { mesh.waterVertexBuffer };
        VkDeviceSize offsets[] = { 0 };
        vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets);
        vkCmdBindIndexBuffer(commandBuffer, mesh.waterIndexBuffer, 0, VK_INDEX_TYPE_UINT32);
        vkCmdDrawIndexed(commandBuffer, mesh.waterIndexCount, 1, 0, 0, 0);
    }

    // Reset to camera-relative model (yAnimOff=0) for non-chunk geometry
    {
        auto m = makeCamRelModel(0.0f);
        vkCmdPushConstants(commandBuffer, pipelineLayout_, VK_SHADER_STAGE_VERTEX_BIT, 0, 64, m.data());
    }

    // Break overlay — slightly-expanded faces over the block being broken, texIndex==-8.0
    if (breakMesh_.valid) {
        vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline_);
        VkBuffer brkVbufs[] = { breakMesh_.vertexBuffer };
        VkDeviceSize brkOffs[] = { 0 };
        vkCmdBindVertexBuffers(commandBuffer, 0, 1, brkVbufs, brkOffs);
        vkCmdBindIndexBuffer(commandBuffer, breakMesh_.indexBuffer, 0, VK_INDEX_TYPE_UINT32);
        vkCmdDrawIndexed(commandBuffer, breakMesh_.indexCount, 1, 0, 0, 0);
    }

    if (outlineMesh_.valid) {
        vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, debugLinePipeline_);
        VkBuffer vertexBuffers[] = { outlineMesh_.vertexBuffer };
        VkDeviceSize offsets[] = { 0 };
        vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets);
        vkCmdBindIndexBuffer(commandBuffer, outlineMesh_.indexBuffer, 0, VK_INDEX_TYPE_UINT32);
        vkCmdDrawIndexed(commandBuffer, outlineMesh_.indexCount, 1, 0, 0, 0);
    }

    // Star/planet/crosshair use the same camera-relative model (already set above).
    // Stars and planet billboards are built with world-space positions = camPos + offset,
    // so after subtracting camPos via the model matrix they become pure direction offsets.

    // Star field — tiny billboard quads at SKY_STAR_DIST from player.
    if (starIndexCount_ > 0 && starVertexBuffer_ != VK_NULL_HANDLE) {
        vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline_);
        VkBuffer vbufs[] = { starVertexBuffer_ };
        VkDeviceSize voffs[] = { 0 };
        vkCmdBindVertexBuffers(commandBuffer, 0, 1, vbufs, voffs);
        vkCmdBindIndexBuffer(commandBuffer, starIndexBuffer_, 0, VK_INDEX_TYPE_UINT32);
        vkCmdDrawIndexed(commandBuffer, static_cast<uint32_t>(starIndexCount_), 1, 0, 0, 0);
    }

    // Sky planet billboards.
    if (skyPlanetIndexCount_ > 0 && skyPlanetVertexBuffer_ != VK_NULL_HANDLE) {
        vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline_);
        VkBuffer vbufs[] = { skyPlanetVertexBuffer_ };
        VkDeviceSize voffs[] = { 0 };
        vkCmdBindVertexBuffers(commandBuffer, 0, 1, vbufs, voffs);
        vkCmdBindIndexBuffer(commandBuffer, skyPlanetIndexBuffer_, 0, VK_INDEX_TYPE_UINT32);
        vkCmdDrawIndexed(commandBuffer, static_cast<uint32_t>(skyPlanetIndexCount_), 1, 0, 0, 0);
    }

    // Cloud layer — flat XZ quads at sky altitude, procedural shape in frag shader.
    if (cloudIndexCount_ > 0 && cloudVertexBuffer_ != VK_NULL_HANDLE) {
        vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline_);
        VkBuffer vbufs[] = { cloudVertexBuffer_ };
        VkDeviceSize voffs[] = { 0 };
        vkCmdBindVertexBuffers(commandBuffer, 0, 1, vbufs, voffs);
        vkCmdBindIndexBuffer(commandBuffer, cloudIndexBuffer_, 0, VK_INDEX_TYPE_UINT32);
        vkCmdDrawIndexed(commandBuffer, static_cast<uint32_t>(cloudIndexCount_), 1, 0, 0, 0);
    }


    // Mob geometry — coloured boxes, world-space positions, texIndex==-6.0
    if (mobDrawIndexCount_ > 0 && mobVertexBuffer_ != VK_NULL_HANDLE) {
        vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline_);
        VkBuffer vbufs[] = { mobVertexBuffer_ };
        VkDeviceSize voffs[] = { 0 };
        vkCmdBindVertexBuffers(commandBuffer, 0, 1, vbufs, voffs);
        vkCmdBindIndexBuffer(commandBuffer, mobIndexBuffer_, 0, VK_INDEX_TYPE_UINT32);
        vkCmdDrawIndexed(commandBuffer, static_cast<uint32_t>(mobDrawIndexCount_), 1, 0, 0, 0);
    }

    // Dropped items — billboard quads, texIndex==-7.0
    if (dropIndexCount_ > 0 && dropVertexBuffer_ != VK_NULL_HANDLE) {
        vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline_);
        VkBuffer vbufs[] = { dropVertexBuffer_ };
        VkDeviceSize voffs[] = { 0 };
        vkCmdBindVertexBuffers(commandBuffer, 0, 1, vbufs, voffs);
        vkCmdBindIndexBuffer(commandBuffer, dropIndexBuffer_, 0, VK_INDEX_TYPE_UINT32);
        vkCmdDrawIndexed(commandBuffer, static_cast<uint32_t>(dropIndexCount_), 1, 0, 0, 0);
    }

    // Crosshair — vertex shader uses texIndex==-2 to bypass MVP entirely.
    if (crosshairVertexBuffer_ != VK_NULL_HANDLE) {
        vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline_);
        VkBuffer vertexBuffers[] = { crosshairVertexBuffer_ };
        VkDeviceSize offsets[] = { 0 };
        vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets);
        vkCmdBindIndexBuffer(commandBuffer, crosshairIndexBuffer_, 0, VK_INDEX_TYPE_UINT32);
        vkCmdDrawIndexed(commandBuffer, 3, 1, 0, 0, 0);
    }

    vkCmdEndRenderPass(commandBuffer);

    if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {
        throw std::runtime_error("vkEndCommandBuffer failed");
    }
}
void App::mat4Inverse(const float m[16], float out[16]) {
    float inv[16];
    inv[0]  =  m[5]*m[10]*m[15] - m[5]*m[11]*m[14] - m[9]*m[6]*m[15] + m[9]*m[7]*m[14] + m[13]*m[6]*m[11] - m[13]*m[7]*m[10];
    inv[4]  = -m[4]*m[10]*m[15] + m[4]*m[11]*m[14] + m[8]*m[6]*m[15] - m[8]*m[7]*m[14] - m[12]*m[6]*m[11] + m[12]*m[7]*m[10];
    inv[8]  =  m[4]*m[9]*m[15]  - m[4]*m[11]*m[13] - m[8]*m[5]*m[15] + m[8]*m[7]*m[13] + m[12]*m[5]*m[11] - m[12]*m[7]*m[9];
    inv[12] = -m[4]*m[9]*m[14]  + m[4]*m[10]*m[13] + m[8]*m[5]*m[14] - m[8]*m[6]*m[13] - m[12]*m[5]*m[10] + m[12]*m[6]*m[9];
    inv[1]  = -m[1]*m[10]*m[15] + m[1]*m[11]*m[14] + m[9]*m[2]*m[15] - m[9]*m[3]*m[14] - m[13]*m[2]*m[11] + m[13]*m[3]*m[10];
    inv[5]  =  m[0]*m[10]*m[15] - m[0]*m[11]*m[14] - m[8]*m[2]*m[15] + m[8]*m[3]*m[14] + m[12]*m[2]*m[11] - m[12]*m[3]*m[10];
    inv[9]  = -m[0]*m[9]*m[15]  + m[0]*m[11]*m[13] + m[8]*m[1]*m[15] - m[8]*m[3]*m[13] - m[12]*m[1]*m[11] + m[12]*m[3]*m[9];
    inv[13] =  m[0]*m[9]*m[14]  - m[0]*m[10]*m[13] - m[8]*m[1]*m[14] + m[8]*m[2]*m[13] + m[12]*m[1]*m[10] - m[12]*m[2]*m[9];
    inv[2]  =  m[1]*m[6]*m[15]  - m[1]*m[7]*m[14]  - m[5]*m[2]*m[15] + m[5]*m[3]*m[14] + m[13]*m[2]*m[7]  - m[13]*m[3]*m[6];
    inv[6]  = -m[0]*m[6]*m[15]  + m[0]*m[7]*m[14]  + m[4]*m[2]*m[15] - m[4]*m[3]*m[14] - m[12]*m[2]*m[7]  + m[12]*m[3]*m[6];
    inv[10] =  m[0]*m[5]*m[15]  - m[0]*m[7]*m[13]  - m[4]*m[1]*m[15] + m[4]*m[3]*m[13] + m[12]*m[1]*m[7]  - m[12]*m[3]*m[5];
    inv[14] = -m[0]*m[5]*m[14]  + m[0]*m[6]*m[13]  + m[4]*m[1]*m[14] - m[4]*m[2]*m[13] - m[12]*m[1]*m[6]  + m[12]*m[2]*m[5];
    inv[3]  = -m[1]*m[6]*m[11]  + m[1]*m[7]*m[10]  + m[5]*m[2]*m[11] - m[5]*m[3]*m[10] - m[9]*m[2]*m[7]   + m[9]*m[3]*m[6];
    inv[7]  =  m[0]*m[6]*m[11]  - m[0]*m[7]*m[10]  - m[4]*m[2]*m[11] + m[4]*m[3]*m[10] + m[8]*m[2]*m[7]   - m[8]*m[3]*m[6];
    inv[11] = -m[0]*m[5]*m[11]  + m[0]*m[7]*m[9]   + m[4]*m[1]*m[11] - m[4]*m[3]*m[9]  - m[8]*m[1]*m[7]   + m[8]*m[3]*m[5];
    inv[15] =  m[0]*m[5]*m[10]  - m[0]*m[6]*m[9]   - m[4]*m[1]*m[10] + m[4]*m[2]*m[9]  + m[8]*m[1]*m[6]   - m[8]*m[2]*m[5];

    float det = m[0]*inv[0] + m[1]*inv[4] + m[2]*inv[8] + m[3]*inv[12];
    if (std::abs(det) < 1e-8f) { std::memcpy(out, m, 64); return; }
    det = 1.0f / det;
    for (int i = 0; i < 16; i++) out[i] = inv[i] * det;
}

// ---- Sky planet billboards -----------------------------------------------
// Biome colours used to tint planet discs.
void App::createSyncObjects() {
    VkSemaphoreCreateInfo semaphoreInfo{};
    semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;

    VkFenceCreateInfo fenceInfo{};
    fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
    fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;

    for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) {
        if (vkCreateSemaphore(device_, &semaphoreInfo, nullptr, &imageAvailableSemaphores_[i]) != VK_SUCCESS ||
            vkCreateSemaphore(device_, &semaphoreInfo, nullptr, &renderFinishedSemaphores_[i]) != VK_SUCCESS ||
            vkCreateFence(device_, &fenceInfo, nullptr, &inFlightFences_[i]) != VK_SUCCESS) {
            throw std::runtime_error("Failed to create sync objects");
        }
    }
}
