#include "app.h"

#include "chunk_generator.h"
#include "planet.h"
#include "world_manager.h"
#include "world_save.h"

#include <algorithm>
#include <cmath>
#include <stdexcept>
#include <string>
#include <fstream>

// ── Load nickname from player.cfg (generated by license server) ───────────
static void loadLicenseConfig() {
    // Look for player.cfg next to the executable
    std::ifstream f("player.cfg");
    if (!f.is_open()) return;
    std::string line;
    while (std::getline(f, line)) {
        // Simple JSON parse: find "nickname":"value"
        const std::string key = "\"nickname\"";
        auto pos = line.find(key);
        if (pos == std::string::npos) continue;
        pos = line.find('"', pos + key.size());
        if (pos == std::string::npos) continue;
        pos++;  // skip opening quote
        auto end = line.find('"', pos);
        if (end == std::string::npos) continue;
        std::string nick = line.substr(pos, end - pos);
        if (!nick.empty()) {
            WorldManager::setNickname(nick);
        }
        break;
    }
}
namespace {

// ---- Sphere vs solid-block collision ----------------------------------------
// Returns true if a sphere of radius r centred at (cx,cy,cz) overlaps any
// solid block in the world.
static bool sphereVsWorld(const World& world, float cx, float cy, float cz, float r,
                           bool unloadedIsSolid = true) {
    const int x0 = static_cast<int>(std::floor(cx - r));
    const int y0 = static_cast<int>(std::floor(cy - r));
    const int z0 = static_cast<int>(std::floor(cz - r));
    const int x1 = static_cast<int>(std::floor(cx + r));
    const int y1 = static_cast<int>(std::floor(cy + r));
    const int z1 = static_cast<int>(std::floor(cz + r));
    for (int z = z0; z <= z1; ++z)
    for (int y = y0; y <= y1; ++y)
    for (int x = x0; x <= x1; ++x) {
        // Check if chunk is loaded; if not, treat as solid (prevents falling out of world)
        if (unloadedIsSolid) {
            const ChunkPos cp = world.worldToChunkPos(x, y, z);
            if (!world.hasChunk(cp)) {
                float nx = std::max(static_cast<float>(x), std::min(cx, static_cast<float>(x + 1)));
                float ny = std::max(static_cast<float>(y), std::min(cy, static_cast<float>(y + 1)));
                float nz = std::max(static_cast<float>(z), std::min(cz, static_cast<float>(z + 1)));
                float dx = cx - nx, dy = cy - ny, dz = cz - nz;
                if (dx*dx + dy*dy + dz*dz < r*r) return true;
                continue;
            }
        }
        if (!isSolid(world.getBlock(x, y, z))) continue;
        // Closest point on block AABB to sphere centre
        float nx = std::max(static_cast<float>(x), std::min(cx, static_cast<float>(x + 1)));
        float ny = std::max(static_cast<float>(y), std::min(cy, static_cast<float>(y + 1)));
        float nz = std::max(static_cast<float>(z), std::min(cz, static_cast<float>(z + 1)));
        float dx = cx - nx, dy = cy - ny, dz = cz - nz;
        if (dx*dx + dy*dy + dz*dz < r*r) return true;
    }
    return false;
}

// Capsule collision: two spheres representing feet and head.
// eyePos is the camera/eye position; gravUp is normalised "up" direction.
static bool collidesWithWorld(const World& world, const Vec3& eyePos, const Vec3& gravUp) {
    constexpr float R       = 0.25f; // capsule radius  (0.5 blocks wide → fits 1-wide tunnels)
    constexpr float EYE_H   = 1.45f; // feet sphere: how far below eye
    constexpr float HEAD_H  = 0.08f; // head sphere: how far below eye
    // Total height: R + EYE_H + (EYE_H→HEAD_H gap) + R ≈ 1.87 → fits 2-block tunnels

    return sphereVsWorld(world,
            eyePos.x - gravUp.x * EYE_H,
            eyePos.y - gravUp.y * EYE_H,
            eyePos.z - gravUp.z * EYE_H, R)
        || sphereVsWorld(world,
            eyePos.x - gravUp.x * HEAD_H,
            eyePos.y - gravUp.y * HEAD_H,
            eyePos.z - gravUp.z * HEAD_H, R);
}

// (old AABB collision replaced by sphereVsWorld / collidesWithWorld above)

const char* blockTypeName(BlockType type) {
    switch (type) {
        case BlockType::Grass:     return "Grass";
        case BlockType::Dirt:      return "Dirt";
        case BlockType::Stone:     return "Stone";
        case BlockType::Sand:      return "Sand";
        case BlockType::Snow:      return "Snow";
        case BlockType::Wood:      return "Wood";
        case BlockType::Leaves:    return "Leaves";
        case BlockType::Sandstone: return "Sandstone";
        case BlockType::Ice:       return "Ice";
        case BlockType::Water:     return "Water";
        case BlockType::Gravel:    return "Gravel";
        case BlockType::Air:
        default:                   return "Air";
    }
}

} // namespace

App::~App() {
    // Ensure workers are stopped even if run() threw before cleanup() was called.
    if (genRunning_) {
        {
            std::lock_guard lock(genMutex_);
            genRunning_ = false;
        }
        genCv_.notify_all();
        for (auto& t : genWorkers_) if (t.joinable()) t.join();
    }
}

void App::run() {
    loadLicenseConfig();   // read nickname from player.cfg if present
    initWindow();
    initVulkan();
    audio_.init();
    mainLoop();
    cleanup();
}

void App::initVulkan() {
    createInstance();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapchain();
    createImageViews();
    createRenderPass();
    createDescriptorSetLayout();
    createGraphicsPipeline();
    createCommandPool();
    createTextureAtlasResources();
    createDepthResources();
    createFramebuffers();

    // Load world list for main menu
    worldList_ = WorldManager::listWorlds();

    // Start background chunk generation workers (idle until a world is selected)
    genRunning_ = true;
    const unsigned nw = std::max(1u, std::thread::hardware_concurrency() - 1u);
    genWorkers_.reserve(nw);
    for (unsigned i = 0; i < nw; ++i)
        genWorkers_.emplace_back([this]{ workerLoop(); });

    // Note: updateChunkStreaming() is NOT called here; it will be called in startWorld().

    createUniformBuffers();
    createDescriptorPool();
    createDescriptorSets();
    createCommandBuffers();
    createCrosshairMesh();
    createSkyPlanetBuffers();
    createStarBuffers();
    createCloudBuffers();

    createMobBuffers();
    createDropBuffers();
    createSyncObjects();
    startTime_ = std::chrono::steady_clock::now();
    lastFrameTime_ = startTime_;
}
void App::updateCamera(float dt) {
    // ESC is now handled by keyCallback (pause / menu transitions).

    // ---- Update gravity direction from nearest planet -----------------------
    // Use a 0.4 s delay before committing to a new face.
    // This prevents oscillation when the player is near a cube-planet edge
    // (where two face normals are nearly equal), which caused left/right shimmer
    // and a "stuck" feedback loop.
    {
        constexpr float GRAVITY_SWITCH_DELAY = 0.4f;
        Vec3 raw;
        PlanetManager::gravityUp(
            camera_.position.x, camera_.position.y, camera_.position.z,
            raw.x, raw.y, raw.z);

        const bool same = (raw.x == pendingGravityUp_.x &&
                           raw.y == pendingGravityUp_.y &&
                           raw.z == pendingGravityUp_.z);
        if (same) {
            gravityChangeTimer_ += dt;
        } else {
            pendingGravityUp_ = raw;
            gravityChangeTimer_ = 0.0f;
        }
        // Commit the switch only once the raw direction has been stable long enough.
        if (gravityChangeTimer_ >= GRAVITY_SWITCH_DELAY) {
            gravityUp_ = pendingGravityUp_;
        }
    }
    camera_.worldUp = gravityUp_;

    static bool flyMode   = false;
    static bool fWasDown  = false;
    static bool vWasDown  = false;
    static bool grounded  = false;

    const bool fNow = glfwGetKey(window_, GLFW_KEY_F) == GLFW_PRESS;
    if (fNow && !fWasDown) {
        flyMode = !flyMode;
        flyModeActive_ = flyMode;
        flySpeedLevel_ = 0;
        verticalVelocity_ = 0.0f;
        flyVelocity_ = {0.0f, 0.0f, 0.0f};
        updateWindowTitle();
    }
    fWasDown = fNow;

    const bool vNow = glfwGetKey(window_, GLFW_KEY_V) == GLFW_PRESS;
    if (vNow && !vWasDown) {
        voxelEditMode_ = !voxelEditMode_;
        updateWindowTitle();
    }
    vWasDown = vNow;

    // G — teleport to the aimed planet
    static bool gWasDown = false;
    const bool gNow = glfwGetKey(window_, GLFW_KEY_G) == GLFW_PRESS;
    if (gNow && !gWasDown && hasPlanetAim_ && aimedPlanetIdx_ >= 0
        && aimedPlanetIdx_ < (int)nearbyPlanets_.size()) {
        const Planet& dest = nearbyPlanets_[aimedPlanetIdx_].planet;
        // Spawn just above maximum terrain height (RELIEF_AMP_MAX = 120) so the
        // landing chunk is always inside the streaming radius (10 chunks = 160 blocks).
        cameraX_ = dest.cx;
        cameraY_ = dest.cy + static_cast<double>(dest.baseRadius) + 140.0;
        cameraZ_ = dest.cz;
        camera_.position.x = static_cast<float>(cameraX_);
        camera_.position.y = static_cast<float>(cameraY_);
        camera_.position.z = static_cast<float>(cameraZ_);
        verticalVelocity_ = 0.0f;
        camera_.orientForward = {0.0f, 0.0f, -1.0f};
        camera_.worldUp       = {0.0f, 1.0f,  0.0f};
        gravityUp_            = {0.0f, 1.0f,  0.0f};
        pendingGravityUp_     = {0.0f, 1.0f,  0.0f};
        gravityChangeTimer_   = 0.4f;
        // Freeze the player until the chunk directly below finishes generating.
        warpUntil_ = currentTime_ + 60.0f; // safety cap
        updateChunkStreaming();
    }
    gWasDown = gNow;

    if (flyMode) {
        constexpr double FLY_SPEEDS[] = { 30.0, 150.0, 600.0, 2500.0 };
        const double speed = FLY_SPEEDS[flySpeedLevel_];

        const bool wKey  = !inventoryOpen_ && glfwGetKey(window_, GLFW_KEY_W)          == GLFW_PRESS;
        const bool sKey  = !inventoryOpen_ && glfwGetKey(window_, GLFW_KEY_S)          == GLFW_PRESS;
        const bool aKey  = !inventoryOpen_ && glfwGetKey(window_, GLFW_KEY_A)          == GLFW_PRESS;
        const bool dKey  = !inventoryOpen_ && glfwGetKey(window_, GLFW_KEY_D)          == GLFW_PRESS;
        const bool upKey = !inventoryOpen_ && glfwGetKey(window_, GLFW_KEY_SPACE)      == GLFW_PRESS;
        const bool dnKey = !inventoryOpen_ && glfwGetKey(window_, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS;

        const Vec3 fwd = camera_.forward();
        const Vec3 rgt = camera_.right();
        const Vec3 up  = camera_.worldUp;

        // Build desired velocity direction from key input
        double dx = 0.0, dy = 0.0, dz = 0.0;
        if (wKey)  { dx += fwd.x; dy += fwd.y; dz += fwd.z; }
        if (sKey)  { dx -= fwd.x; dy -= fwd.y; dz -= fwd.z; }
        if (aKey)  { dx -= rgt.x; dy -= rgt.y; dz -= rgt.z; }
        if (dKey)  { dx += rgt.x; dy += rgt.y; dz += rgt.z; }
        if (upKey) { dx += up.x;  dy += up.y;  dz += up.z;  }
        if (dnKey) { dx -= up.x;  dy -= up.y;  dz -= up.z;  }

        const double len = std::sqrt(dx*dx + dy*dy + dz*dz);

        // Small but noticeable inertia: snappy acceleration, short coast on release
        constexpr float FLY_ACCEL  = 14.0f; // s⁻¹
        constexpr float FLY_DAMPEN =  9.0f; // s⁻¹  (~0.5 s to stop; distance ≈ speed/9)
        if (len > 1e-9) {
            const float tgtX = static_cast<float>(dx / len * speed);
            const float tgtY = static_cast<float>(dy / len * speed);
            const float tgtZ = static_cast<float>(dz / len * speed);
            flyVelocity_.x += (tgtX - flyVelocity_.x) * std::min(1.0f, FLY_ACCEL * dt);
            flyVelocity_.y += (tgtY - flyVelocity_.y) * std::min(1.0f, FLY_ACCEL * dt);
            flyVelocity_.z += (tgtZ - flyVelocity_.z) * std::min(1.0f, FLY_ACCEL * dt);
        } else {
            const float damp = std::max(0.0f, 1.0f - FLY_DAMPEN * dt);
            flyVelocity_.x *= damp;
            flyVelocity_.y *= damp;
            flyVelocity_.z *= damp;
        }

        // Fly collision: small sphere (R=0.3) — fits through 1-block holes.
        // Unloaded chunks are treated as passable so the camera can fly freely
        // into ungenerated areas without being blocked by invisible walls.
        constexpr float FLY_R    = 0.3f;
        constexpr float FLY_STEP = 0.05f;

        auto flyCollidesAt = [&](float px, float py, float pz) -> bool {
            return sphereVsWorld(world_, px, py, pz, FLY_R, /*unloadedIsSolid=*/false);
        };

        // Sweep each axis
        auto flySweep = [&](double& pos, float vel) {
            double disp = static_cast<double>(vel) * static_cast<double>(dt);
            while (std::abs(disp) > 1e-9) {
                double d = std::clamp(disp, -(double)FLY_STEP, (double)FLY_STEP);
                double npos = pos + d;
                float tx = static_cast<float>(cameraX_);
                float ty = static_cast<float>(cameraY_);
                float tz = static_cast<float>(cameraZ_);
                if      (&pos == &cameraX_) tx = static_cast<float>(npos);
                else if (&pos == &cameraY_) ty = static_cast<float>(npos);
                else                        tz = static_cast<float>(npos);
                if (!flyCollidesAt(tx, ty, tz)) {
                    pos = npos;
                    disp -= d;
                } else {
                    // Zero out the component of flyVelocity_ along this axis
                    if      (&pos == &cameraX_) flyVelocity_.x = 0.0f;
                    else if (&pos == &cameraY_) flyVelocity_.y = 0.0f;
                    else                        flyVelocity_.z = 0.0f;
                    break;
                }
            }
        };

        flySweep(cameraX_, flyVelocity_.x);
        flySweep(cameraY_, flyVelocity_.y);
        flySweep(cameraZ_, flyVelocity_.z);

        camera_.position.x = static_cast<float>(cameraX_);
        camera_.position.y = static_cast<float>(cameraY_);
        camera_.position.z = static_cast<float>(cameraZ_);
        return;
    }

    // ---- Compute planet-relative horizontal directions ----------------------
    // Project camera forward and right onto the planet surface plane.
    const Vec3 gu = gravityUp_;

    auto projectFlat = [&](Vec3 v) -> Vec3 {
        float d = v.x*gu.x + v.y*gu.y + v.z*gu.z;
        Vec3 flat{v.x - gu.x*d, v.y - gu.y*d, v.z - gu.z*d};
        float len = std::sqrt(flat.x*flat.x + flat.y*flat.y + flat.z*flat.z);
        if (len > 1e-6f) { flat.x /= len; flat.y /= len; flat.z /= len; }
        return flat;
    };

    Vec3 flatForward = projectFlat(camera_.forward());
    Vec3 flatRight   = projectFlat(camera_.right());

    // ---- Build desired horizontal displacement ------------------------------
    Vec3 horizDir{0.0f, 0.0f, 0.0f};
    if (!inventoryOpen_) {
    if (glfwGetKey(window_, GLFW_KEY_W) == GLFW_PRESS) { horizDir.x += flatForward.x; horizDir.y += flatForward.y; horizDir.z += flatForward.z; }
    if (glfwGetKey(window_, GLFW_KEY_S) == GLFW_PRESS) { horizDir.x -= flatForward.x; horizDir.y -= flatForward.y; horizDir.z -= flatForward.z; }
    if (glfwGetKey(window_, GLFW_KEY_A) == GLFW_PRESS) { horizDir.x -= flatRight.x;   horizDir.y -= flatRight.y;   horizDir.z -= flatRight.z; }
    if (glfwGetKey(window_, GLFW_KEY_D) == GLFW_PRESS) { horizDir.x += flatRight.x;   horizDir.y += flatRight.y;   horizDir.z += flatRight.z; }
    }

    float horizLen = std::sqrt(horizDir.x*horizDir.x + horizDir.y*horizDir.y + horizDir.z*horizDir.z);
    if (horizLen > 1e-6f) { horizDir.x /= horizLen; horizDir.y /= horizLen; horizDir.z /= horizLen; }

    // ---- Grounded check: probe slightly in -gravityUp direction -------------
    constexpr float PROBE_DIST = 0.08f;
    {
        Vec3 probe{
            static_cast<float>(cameraX_) - gu.x * PROBE_DIST,
            static_cast<float>(cameraY_) - gu.y * PROBE_DIST,
            static_cast<float>(cameraZ_) - gu.z * PROBE_DIST};
        grounded = collidesWithWorld(world_, probe, gu);
    }

    // Shift = sneak (priority over Ctrl), Ctrl = sprint
    const bool sneakHeld = glfwGetKey(window_, GLFW_KEY_LEFT_SHIFT)   == GLFW_PRESS
                        || glfwGetKey(window_, GLFW_KEY_RIGHT_SHIFT)  == GLFW_PRESS;
    const bool sprintHeld = !sneakHeld &&
                           (glfwGetKey(window_, GLFW_KEY_LEFT_CONTROL)  == GLFW_PRESS
                         || glfwGetKey(window_, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS);
    const float walkSpeed  = sneakHeld ? 2.5f : (sprintHeld ? 15.0f : 5.0f);
    constexpr float gravity    = 22.0f;
    constexpr float jumpSpeed  = 8.5f;
    constexpr float step       = 0.05f;

    // ---- Water detection ----------------------------------------------------
    const int eyeX  = static_cast<int>(std::floor(cameraX_));
    const int eyeY  = static_cast<int>(std::floor(cameraY_));
    const int eyeZ  = static_cast<int>(std::floor(cameraZ_));
    const int feetX = static_cast<int>(std::floor(cameraX_ - gu.x * 1.5));
    const int feetY = static_cast<int>(std::floor(cameraY_ - gu.y * 1.5));
    const int feetZ = static_cast<int>(std::floor(cameraZ_ - gu.z * 1.5));
    const bool eyeInWater  = world_.hasWater(eyeX,  eyeY,  eyeZ);
    const bool bodyInWater = eyeInWater || world_.hasWater(feetX, feetY, feetZ);

    // Effective horizontal speed: slowed in water
    const float effectiveWalkSpeed = bodyInWater ? walkSpeed * 0.45f : walkSpeed;

    // ---- Gravity / swimming -------------------------------------------------
    if (bodyInWater) {
        // Buoyancy: weak downward pull, capped fall speed
        constexpr float WATER_GRAV     =  4.5f;
        constexpr float WATER_SWIM_ACC = 14.0f; // acceleration when pressing up/down
        constexpr float WATER_MAX_UP   =  4.5f;
        constexpr float WATER_MAX_DOWN =  2.5f;

        if (!grounded) {
            verticalVelocity_ -= WATER_GRAV * dt;
            if (verticalVelocity_ < -WATER_MAX_DOWN) verticalVelocity_ = -WATER_MAX_DOWN;
        } else if (verticalVelocity_ < 0.0f) {
            verticalVelocity_ = 0.0f;
        }
        // Space: swim up
        if (!inventoryOpen_ && glfwGetKey(window_, GLFW_KEY_SPACE) == GLFW_PRESS) {
            verticalVelocity_ += WATER_SWIM_ACC * dt;
            if (verticalVelocity_ > WATER_MAX_UP) verticalVelocity_ = WATER_MAX_UP;
        }
        // Shift (sneak in water = dive down)
        if (sneakHeld) {
            verticalVelocity_ -= WATER_SWIM_ACC * dt;
            if (verticalVelocity_ < -WATER_MAX_DOWN) verticalVelocity_ = -WATER_MAX_DOWN;
        }
    } else {
        // Normal jump + gravity
        if (grounded && !inventoryOpen_ && glfwGetKey(window_, GLFW_KEY_SPACE) == GLFW_PRESS) {
            verticalVelocity_ = jumpSpeed;
            grounded = false;
        }

        // Post-teleport: freeze the player in the air until the chunk directly below
        // finishes generating. Once that chunk is loaded, full gravity starts immediately
        // and the player falls and lands normally. No timers, no fake gravity.
        if (currentTime_ < warpUntil_) {
            const int warpFeetY = static_cast<int>(std::floor(
                camera_.position.y - gu.y * 1.6f));
            const ChunkPos belowChunk = world_.worldToChunkPos(
                static_cast<int>(camera_.position.x),
                warpFeetY,
                static_cast<int>(camera_.position.z));
            const bool chunkReady = (world_.getChunk(belowChunk) != nullptr);
            if (chunkReady) {
                warpUntil_ = 0.0f;
            } else {
                verticalVelocity_ = 0.0f;
            }
        }
        if (currentTime_ >= warpUntil_) {
            if (!grounded)              verticalVelocity_ -= gravity * dt;
            else if (verticalVelocity_ < 0.0f) verticalVelocity_ = 0.0f;
        }
    }

    // Total displacement (computed in float — delta is always small, no precision issue)
    const double dispX = horizDir.x * effectiveWalkSpeed * dt + gu.x * verticalVelocity_ * dt;
    const double dispY = horizDir.y * effectiveWalkSpeed * dt + gu.y * verticalVelocity_ * dt;
    const double dispZ = horizDir.z * effectiveWalkSpeed * dt + gu.z * verticalVelocity_ * dt;

    // Save position before sweep — needed for sneak edge prevention
    const double preSweepX = cameraX_, preSweepY = cameraY_, preSweepZ = cameraZ_;
    const bool wasGrounded  = grounded;

    // ---- Sweep X, Y, Z in double; collision detection via float cast ----------
    // Accumulating in double means position is precise at any coordinate.
    // camera_.position (float) is synced after each axis so collision detection
    // (which only cares about which integer block we're in) stays correct.
    auto sweepAxis = [&](double& pos, double disp, float gu_component) {
        double remaining = disp;
        while (std::abs(remaining) > 1e-9) {
            double d = std::clamp(remaining, -(double)step, (double)step);
            double newPos = pos + d;
            Vec3 test = camera_.position;
            // Only the relevant axis changes in the collision probe
            if      (&pos == &cameraX_) test.x = static_cast<float>(newPos);
            else if (&pos == &cameraY_) test.y = static_cast<float>(newPos);
            else                        test.z = static_cast<float>(newPos);
            if (!collidesWithWorld(world_, test, gu)) {
                pos = newPos;
                remaining -= d;
            } else {
                if (d * gu_component < 0.0f) { grounded = true; verticalVelocity_ = 0.0f; }
                break;
            }
        }
    };
    sweepAxis(cameraX_, dispX, gu.x);
    camera_.position.x = static_cast<float>(cameraX_);
    sweepAxis(cameraY_, dispY, gu.y);
    camera_.position.y = static_cast<float>(cameraY_);
    sweepAxis(cameraZ_, dispZ, gu.z);
    camera_.position.z = static_cast<float>(cameraZ_);

    // ---- Footstep sound ────────────────────────────────────────────────────
    if (grounded && !flyModeActive_ && horizLen > 0.1f) {
        footstepAccum_ += horizLen * dt;
        if (footstepAccum_ >= 1.8f) {
            audio_.playFootstep();
            footstepAccum_ = 0.0f;
        }
    }

    // ---- Sneak: prevent walking off edges -----------------------------------
    // If shift is held and the player was grounded and moved horizontally,
    // check whether there is still ground under the new position. If not,
    // roll back to the pre-sweep position so the player hugs the edge.
    if (sneakHeld && wasGrounded && horizLen > 1e-6f && !bodyInWater) {
        constexpr float SNEAK_PROBE = 0.08f;
        Vec3 sneakProbe{
            static_cast<float>(cameraX_) - gu.x * SNEAK_PROBE,
            static_cast<float>(cameraY_) - gu.y * SNEAK_PROBE,
            static_cast<float>(cameraZ_) - gu.z * SNEAK_PROBE};
        if (!collidesWithWorld(world_, sneakProbe, gu)) {
            cameraX_ = preSweepX;
            cameraY_ = preSweepY;
            cameraZ_ = preSweepZ;
            camera_.position.x = static_cast<float>(cameraX_);
            camera_.position.y = static_cast<float>(cameraY_);
            camera_.position.z = static_cast<float>(cameraZ_);
        }
    }

    // ---- Push out of solid if stuck -----------------------------------------
    if (collidesWithWorld(world_, camera_.position, gu)) {
        for (int i = 0; i < 24; ++i) {
            cameraX_ += gu.x * 0.1;
            cameraY_ += gu.y * 0.1;
            cameraZ_ += gu.z * 0.1;
            camera_.position.x = static_cast<float>(cameraX_);
            camera_.position.y = static_cast<float>(cameraY_);
            camera_.position.z = static_cast<float>(cameraZ_);
            if (!collidesWithWorld(world_, camera_.position, gu)) break;
        }
        verticalVelocity_ = 0.0f;
    }
}

void App::mainLoop() {
    while (!glfwWindowShouldClose(window_)) {
        glfwPollEvents();

        auto now = std::chrono::steady_clock::now();
        float dt = std::chrono::duration<float>(now - lastFrameTime_).count();
        currentTime_ = std::chrono::duration<float>(now - startTime_).count();
        lastFrameTime_ = now;
        if (dt > 0.05f) dt = 0.05f;

        // Camera velocity for odometer animation gating (blocks / second)
        if (dt > 0.0f) {
            constexpr float kSmooth = 8.0f; // low-pass factor (higher = faster response)
            float vx = static_cast<float>((cameraX_ - prevCamX_) / dt);
            float vy = static_cast<float>((cameraY_ - prevCamY_) / dt);
            float vz = static_cast<float>((cameraZ_ - prevCamZ_) / dt);
            camVelX_ += (vx - camVelX_) * std::min(1.0f, kSmooth * dt);
            camVelY_ += (vy - camVelY_) * std::min(1.0f, kSmooth * dt);
            camVelZ_ += (vz - camVelZ_) * std::min(1.0f, kSmooth * dt);
        }
        prevCamX_ = cameraX_; prevCamY_ = cameraY_; prevCamZ_ = cameraZ_;

        // Accumulate per-world playtime while actively playing
        if (gameState_ == GameState::Playing) worldPlayTime_ += dt;

        // Cursor blink (toggle every 0.4 s)
        cursorBlinkTimer_ += dt;
        if (cursorBlinkTimer_ >= 0.4f) {
            cursorBlinkTimer_ = 0.0f;
            cursorBlink_ = 1 - cursorBlink_;
        }

        switch (gameState_) {
            case GameState::MainMenu:
                handleMainMenu(dt);
                break;

            case GameState::Playing:
                updateHotbarSelection();
                tickLeafDecay();
                updateCamera(dt);
                // ── Air (breath) ──────────────────────────────────────────
                {
                    const bool eyeWet = world_.hasWater(
                        static_cast<int>(std::floor(cameraX_)),
                        static_cast<int>(std::floor(cameraY_)),
                        static_cast<int>(std::floor(cameraZ_)));
                    if (eyeWet) {
                        // Deplete: 100% over 120 s  → 0.8333 %/s
                        airPercent_ -= (100.0f / 120.0f) * dt;
                        if (airPercent_ <= 0.0f) {
                            airPercent_ = 0.0f;
                            // Deal 1 HP/s drowning damage
                            playerHp_ -= 1.0f * dt;
                            if (playerHp_ <= 0.0f) {
                                wolfKilledPlayer_ = true; // reuse death trigger
                            }
                        }
                    } else {
                        // Refill: 100% over 300 s  → 0.3333 %/s  (2.5× slower)
                        airPercent_ = std::min(100.0f, airPercent_ + (100.0f / 300.0f) * dt);
                    }
                }
                updateWindowTitle();
                updateMobs(dt);
                updateDrops(dt);
                if (wolfKilledPlayer_) {
                    wolfKilledPlayer_ = false;
                    std::string dead = activeWorldName_;
                    leaveWorld();
                    WorldManager::deleteWorld(dead);
                    worldList_ = WorldManager::listWorlds();
                    break;
                }
                updateChunkStreaming();
                world_.tickWater(96);
                drainReadyChunks();
                rebuildDirtyWorldChunks();
                updateSelectionOutline();
                handleBlockInteraction(dt);
                if (inventoryOpen_) handleInventoryClick();
                break;

            case GameState::Paused:
                handlePaused(dt);
                drainReadyChunks(); // keep chunks building in background
                break;

            case GameState::Quitting:
                handleQuitting(dt);
                break;
        }

        drawFrame();
    }

    // Save world + position + inventory before final cleanup
    if (gameState_ == GameState::Playing || gameState_ == GameState::Paused) {
        if (!activeWorldName_.empty()) {
            world_.saveAllModified();
            WorldManager::savePlayerPos(activeWorldName_,
                cameraX_, cameraY_, cameraZ_,
                camera_.orientForward.x,
                camera_.orientForward.y,
                camera_.orientForward.z);
            WorldManager::SaveSlot slots[16];
            for (int i = 0; i < 16; ++i) {
                slots[i].type  = static_cast<int32_t>(inventory_[i].type);
                slots[i].count = inventory_[i].count;
            }
            WorldManager::saveInventory(activeWorldName_, slots);
        }
    }

    vkDeviceWaitIdle(device_);
}

void App::cleanup() {
    // Stop background workers
    {
        std::lock_guard lock(genMutex_);
        genRunning_ = false;
    }
    genCv_.notify_all();
    for (auto& t : genWorkers_) t.join();
    genWorkers_.clear();

    cleanupSwapchain();
    destroyChunkMeshes();
    destroyOutlineMesh();
    destroyBreakMesh();
    destroyCrosshairMesh();
    destroySkyPlanetBuffers();
    destroyStarBuffers();
    destroyCloudBuffers();

    destroyMobBuffers();
    destroyDropBuffers();

    // Drain any deferred deletions that accumulated in the game loop
    for (auto& q : deletionQueues_) {
        for (auto& pd : q) {
            if (pd.buf != VK_NULL_HANDLE) vkDestroyBuffer(device_, pd.buf, nullptr);
            if (pd.mem != VK_NULL_HANDLE) vkFreeMemory(device_, pd.mem, nullptr);
        }
        q.clear();
    }
    destroyTextureAtlasResources();

    if (descriptorPool_ != VK_NULL_HANDLE) {
        vkDestroyDescriptorPool(device_, descriptorPool_, nullptr);
    }

    for (size_t i = 0; i < uniformBuffers_.size(); ++i) {
        destroyBuffer(uniformBuffers_[i], uniformBuffersMemory_[i]);
    }

    if (descriptorSetLayout_ != VK_NULL_HANDLE) {
        vkDestroyDescriptorSetLayout(device_, descriptorSetLayout_, nullptr);
    }

    for (int i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) {
        if (imageAvailableSemaphores_[i] != VK_NULL_HANDLE) {
            vkDestroySemaphore(device_, imageAvailableSemaphores_[i], nullptr);
        }
        if (renderFinishedSemaphores_[i] != VK_NULL_HANDLE) {
            vkDestroySemaphore(device_, renderFinishedSemaphores_[i], nullptr);
        }
        if (inFlightFences_[i] != VK_NULL_HANDLE) {
            vkDestroyFence(device_, inFlightFences_[i], nullptr);
        }
    }

    if (commandPool_ != VK_NULL_HANDLE) {
        vkDestroyCommandPool(device_, commandPool_, nullptr);
    }

    if (device_ != VK_NULL_HANDLE) {
        vkDestroyDevice(device_, nullptr);
    }

    if (surface_ != VK_NULL_HANDLE) {
        vkDestroySurfaceKHR(instance_, surface_, nullptr);
    }

    if (instance_ != VK_NULL_HANDLE) {
        vkDestroyInstance(instance_, nullptr);
    }

    audio_.cleanup();

    if (window_) {
        glfwDestroyWindow(window_);
    }

    glfwTerminate();
}

void App::drawFrame() {
    vkWaitForFences(device_, 1, &inFlightFences_[currentFrame_], VK_TRUE, UINT64_MAX);

    // Drain deferred buffer deletions for this frame slot.
    // The fence above guarantees the GPU finished with these buffers MAX_FRAMES_IN_FLIGHT frames ago.
    for (auto& pd : deletionQueues_[currentFrame_]) {
        if (pd.buf != VK_NULL_HANDLE) vkDestroyBuffer(device_, pd.buf, nullptr);
        if (pd.mem != VK_NULL_HANDLE) vkFreeMemory(device_, pd.mem, nullptr);
    }
    deletionQueues_[currentFrame_].clear();

    uint32_t imageIndex = 0;
    VkResult acquireResult = vkAcquireNextImageKHR(
        device_,
        swapchain_,
        UINT64_MAX,
        imageAvailableSemaphores_[currentFrame_],
        VK_NULL_HANDLE,
        &imageIndex
    );

    if (acquireResult == VK_ERROR_OUT_OF_DATE_KHR) {
        recreateSwapchain();
        return;
    }

    if (acquireResult != VK_SUCCESS && acquireResult != VK_SUBOPTIMAL_KHR) {
        throw std::runtime_error("vkAcquireNextImageKHR failed");
    }

    updateUniformBuffer(imageIndex);
    updateSkyPlanetMesh();
    updateStarMesh();
    updateCloudMesh();

    updateMobMesh();
    updateDropMesh();

    vkResetFences(device_, 1, &inFlightFences_[currentFrame_]);
    vkResetCommandBuffer(commandBuffers_[imageIndex], 0);
    recordCommandBuffer(commandBuffers_[imageIndex], imageIndex);

    VkSemaphore waitSemaphores[] = { imageAvailableSemaphores_[currentFrame_] };
    VkPipelineStageFlags waitStages[] = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT };
    VkSemaphore signalSemaphores[] = { renderFinishedSemaphores_[currentFrame_] };

    VkSubmitInfo submitInfo{};
    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submitInfo.waitSemaphoreCount = 1;
    submitInfo.pWaitSemaphores = waitSemaphores;
    submitInfo.pWaitDstStageMask = waitStages;
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &commandBuffers_[imageIndex];
    submitInfo.signalSemaphoreCount = 1;
    submitInfo.pSignalSemaphores = signalSemaphores;

    if (vkQueueSubmit(graphicsQueue_, 1, &submitInfo, inFlightFences_[currentFrame_]) != VK_SUCCESS) {
        throw std::runtime_error("vkQueueSubmit failed");
    }

    VkSwapchainKHR swapchains[] = { swapchain_ };
    VkPresentInfoKHR presentInfo{};
    presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
    presentInfo.waitSemaphoreCount = 1;
    presentInfo.pWaitSemaphores = signalSemaphores;
    presentInfo.swapchainCount = 1;
    presentInfo.pSwapchains = swapchains;
    presentInfo.pImageIndices = &imageIndex;

    VkResult presentResult = vkQueuePresentKHR(presentQueue_, &presentInfo);

    if (presentResult == VK_ERROR_OUT_OF_DATE_KHR ||
        presentResult == VK_SUBOPTIMAL_KHR ||
        framebufferResized_) {
        framebufferResized_ = false;
        recreateSwapchain();
    } else if (presentResult != VK_SUCCESS) {
        throw std::runtime_error("vkQueuePresentKHR failed");
    }

    currentFrame_ = (currentFrame_ + 1) % MAX_FRAMES_IN_FLIGHT;
}

void App::recreateSwapchain() {
    int width = 0;
    int height = 0;
    glfwGetFramebufferSize(window_, &width, &height);

    while (width == 0 || height == 0) {
        glfwGetFramebufferSize(window_, &width, &height);
        glfwWaitEvents();
    }

    vkDeviceWaitIdle(device_);

    cleanupSwapchain();

    createSwapchain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createDepthResources();
    createFramebuffers();
    createCommandBuffers();
}
