#pragma once
#include "audio.h"
#include "mob.h"
#include "planet.h"
#include "world_manager.h"

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>

#include "camera.h"
#include "raycast.h"
#include "world.h"

#include <array>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <tuple>
#include <unordered_set>
#include <vector>

struct Vertex {
    float pos[3];
    float color[3];
    float uv[2];
    float texIndex;

    static VkVertexInputBindingDescription bindingDescription() {
        VkVertexInputBindingDescription desc{};
        desc.binding = 0;
        desc.stride = sizeof(Vertex);
        desc.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
        return desc;
    }

    static std::array<VkVertexInputAttributeDescription, 4> attributeDescriptions() {
        std::array<VkVertexInputAttributeDescription, 4> desc{};

        desc[0].binding = 0;
        desc[0].location = 0;
        desc[0].format = VK_FORMAT_R32G32B32_SFLOAT;
        desc[0].offset = offsetof(Vertex, pos);

        desc[1].binding = 0;
        desc[1].location = 1;
        desc[1].format = VK_FORMAT_R32G32B32_SFLOAT;
        desc[1].offset = offsetof(Vertex, color);

        desc[2].binding = 0;
        desc[2].location = 2;
        desc[2].format = VK_FORMAT_R32G32_SFLOAT;
        desc[2].offset = offsetof(Vertex, uv);

        desc[3].binding = 0;
        desc[3].location = 3;
        desc[3].format = VK_FORMAT_R32_SFLOAT;
        desc[3].offset = offsetof(Vertex, texIndex);

        return desc;
    }
};

struct UniformBufferObject {
    float   mvp[16];       // offset  0, 64 bytes
    float   screenWidth;   // offset 64,  4 bytes
    float   screenHeight;  // offset 68,  4 bytes
    float   skyR;          // offset 72,  4 bytes
    float   skyG;          // offset 76,  4 bytes
    int32_t playerX;       // offset 80,  4 bytes
    int32_t playerY;       // offset 84,  4 bytes
    int32_t playerZ;       // offset 88,  4 bytes
    float   skyB;          // offset 92,  4 bytes
    // Planet label (zero = no label)
    int32_t planetVisible; // offset  96
    int32_t planetScreenX; // offset 100 — screen pixel X of planet centre
    int32_t planetScreenY; // offset 104
    int32_t planetBiome;   // offset 108 — 0..6  (for border colour)
    int32_t planetDist;    // offset 112 — distance in blocks (capped at 9 999 999)
    int32_t planetCX;      // offset 116 — planet centre coords (rounded)
    int32_t planetCY;      // offset 120
    int32_t planetCZ;      // offset 124
    int32_t planetRadius;  // offset 128 — baseRadius in blocks
    // Name: up to 12 chars packed as glyph codes (8 bits each, 4 per word)
    int32_t planetName0;   // offset 132 — chars 0-3
    int32_t planetName1;   // offset 136 — chars 4-7
    int32_t planetName2;   // offset 140 — chars 8-11
    int32_t planetScreenR;     // offset 144 — visual disc radius in screen pixels
    int32_t planetOriginDist;  // offset 148 — distance from world origin (0,0,0) to planet centre

    // ── UI / game state (offset 152+) ──────────────────────────────────────
    int32_t gameState;    // 0=playing, 1=mainmenu, 2=paused, 3=quitting
    int32_t hotbarSlot;   // 0-3 selected hotbar column
    int32_t invOpen;      // 0/1 inventory panel open

    // Inventory: 16 slots, each = (blockType << 16) | count
    int32_t inv0,  inv1,  inv2,  inv3;
    int32_t inv4,  inv5,  inv6,  inv7;
    int32_t inv8,  inv9,  inv10, inv11;
    int32_t inv12, inv13, inv14, inv15;

    // Main-menu state
    int32_t menuSubState;  // 0=world list, 1=create world form
    int32_t menuSel;       // selected world index or option
    int32_t worldCount;    // 0-8

    // World names: 8 worlds × 3 ints each (12 chars per name, packed)
    int32_t wn00, wn01, wn02;
    int32_t wn10, wn11, wn12;
    int32_t wn20, wn21, wn22;
    int32_t wn30, wn31, wn32;
    int32_t wn40, wn41, wn42;
    int32_t wn50, wn51, wn52;
    int32_t wn60, wn61, wn62;
    int32_t wn70, wn71, wn72;

    // Nickname (12 chars = 3 ints)
    int32_t nick0, nick1, nick2;

    // Text-input buffer: name (6 ints = 24 chars) + seed (4 ints = 16 hex chars)
    int32_t tb0, tb1, tb2, tb3, tb4, tb5;
    int32_t textLen;    // chars in name buffer
    int32_t textField;  // 0=name focused, 1=seed focused
    int32_t sb0, sb1, sb2, sb3;
    int32_t seedLen;    // chars in seed buffer

    // Blinking cursor: 0 or 1, toggled at ~2 Hz
    int32_t cursorBlink;

    // Held item (dragged in inventory) + cursor position for rendering
    int32_t heldType;   // BlockType of held item (0 = nothing)
    int32_t heldCount;  // count of held item
    int32_t cursorX;    // cursor X in framebuffer pixels
    int32_t cursorY;    // cursor Y in framebuffer pixels
    int32_t underWater; // 1 if camera eye is inside a water block
    int32_t airPercent; // 0-100 remaining air
    // Camera world position (offsets 428-443)
    float   camWX;      // offset 428
    float   camWY;      // offset 432
    float   camWZ;      // offset 436
    float   camWPad;    // offset 440
    float   camWPad2;   // offset 444  (alignment padding — mat4 needs 16-byte boundary)
    float   _pad448[16]; // offset 448 (reserved padding)
    // Dynamic lighting / time-of-day (offset 512+)
    float   sunDirX;    // offset 512 — normalized world-space sun direction
    float   sunDirY;    // offset 516
    float   sunDirZ;    // offset 520
    float   timeOfDay;        // offset 524 — 0=midnight, 0.25=dawn, 0.5=noon, 0.75=dusk
    float   gameTime;         // offset 528 — seconds since startup (wraps at 3600s)
    int32_t worldPlayTimeSec; // offset 532 — total seconds spent in this world
    float   worldPlayTimeFrac;// offset 536 — fractional second within worldPlayTime (0-1)
    float   camVX;            // offset 540 — camera velocity X (blocks/sec)
    float   camVY;            // offset 544
    float   camVZ;            // offset 548
    float   spaceBlend;       // offset 552 — 0=surface, 1=space (fades clouds/sun/moon)
    // World settings screen
    int32_t settingsWorldSel; // offset 556 — world index shown in settings (-1 = none)
    int32_t deleteConfirm;    // offset 560 — 1=showing delete confirmation
    int32_t playerHp;         // offset 564 — current player HP (0-16)
    float   breakProgress;    // offset 568 — 0=not breaking, 1=fully broken
    int32_t craft0;           // offset 572 — craft grid slot 0
    int32_t craft1;           // offset 576 — craft grid slot 1
    int32_t craft2;           // offset 580 — craft grid slot 2
    int32_t craft3;           // offset 584 — craft grid slot 3
    int32_t craft4;           // offset 588 — craft grid slot 4
    int32_t craft5;           // offset 592 — craft grid slot 5
    int32_t craft6;           // offset 596 — craft grid slot 6
    int32_t craft7;           // offset 600 — craft grid slot 7
    int32_t craft8;           // offset 604 — craft grid slot 8
    int32_t craftOut;         // offset 608 — crafting output slot
    int32_t recipeAvail;      // offset 612 — bitmask: bit i = recipe i craftable
};

struct ChunkGpuMesh {
    ChunkPos pos{};
    int lodLevel = -1;  // -1 = not yet built
    float spawnTime = 0.0f; // game-clock seconds when first uploaded (for rise animation)
    Vec3  spawnUp{0.0f, 1.0f, 0.0f}; // gravity "up" at chunk centre when first uploaded

    VkBuffer opaqueVertexBuffer = VK_NULL_HANDLE;
    VkDeviceMemory opaqueVertexBufferMemory = VK_NULL_HANDLE;
    VkBuffer opaqueIndexBuffer = VK_NULL_HANDLE;
    VkDeviceMemory opaqueIndexBufferMemory = VK_NULL_HANDLE;
    uint32_t opaqueIndexCount = 0;

    VkBuffer waterVertexBuffer = VK_NULL_HANDLE;
    VkDeviceMemory waterVertexBufferMemory = VK_NULL_HANDLE;
    VkBuffer waterIndexBuffer = VK_NULL_HANDLE;
    VkDeviceMemory waterIndexBufferMemory = VK_NULL_HANDLE;
    uint32_t waterIndexCount = 0;
};

struct DebugGpuMesh {
    VkBuffer vertexBuffer = VK_NULL_HANDLE;
    VkDeviceMemory vertexBufferMemory = VK_NULL_HANDLE;
    VkBuffer indexBuffer = VK_NULL_HANDLE;
    VkDeviceMemory indexBufferMemory = VK_NULL_HANDLE;
    uint32_t indexCount = 0;
    bool valid = false;
};

class App {
public:
    void run();
    ~App();  // joins worker threads

private:
    static constexpr uint32_t WINDOW_WIDTH = 1280;
    static constexpr uint32_t WINDOW_HEIGHT = 720;
    static constexpr int MAX_FRAMES_IN_FLIGHT = 2;
    static constexpr int CHUNK_STREAM_RADIUS         = 18;   // total loaded radius (chunks)
    static constexpr int LOD_NEAR_RADIUS             = 10;   // full-detail radius; beyond = LOD 1
    static constexpr int CHUNK_LOADS_PER_FRAME        = 16;  // mesh uploads per frame
    static constexpr int CHUNK_MESH_REBUILDS_PER_FRAME = 64; // dirty rebuilds per frame (fallback)
    static constexpr int CHUNK_UNLOADS_PER_FRAME      = 32;

    struct QueueFamilyIndices {
        std::optional<uint32_t> graphicsFamily;
        std::optional<uint32_t> presentFamily;

        bool complete() const {
            return graphicsFamily.has_value() && presentFamily.has_value();
        }
    };

    struct SwapchainSupportDetails {
        VkSurfaceCapabilitiesKHR capabilities{};
        std::vector<VkSurfaceFormatKHR> formats;
        std::vector<VkPresentModeKHR> presentModes;
    };

    struct ChunkPosQueueHash {
        std::size_t operator()(const ChunkPos& p) const noexcept {
            return static_cast<std::size_t>(
                static_cast<std::uint32_t>(p.x * 73856093) ^
                static_cast<std::uint32_t>(p.y * 19349663) ^
                static_cast<std::uint32_t>(p.z * 83492791)
            );
        }
    };

    GLFWwindow* window_ = nullptr;
    bool framebufferResized_ = false;

    bool firstMouse_ = true;
    double lastMouseX_ = 0.0;
    double lastMouseY_ = 0.0;
    Camera camera_{};
    World  world_{};

    // High-precision camera world position.  float camera_.position is derived
    // from these each frame and used only for collision (block coords are ints).
    double cameraX_ = 0.0, cameraY_ = 20.0, cameraZ_ = 0.0;

    float spaceBlend_ = 0.0f;   // 0=surface, 1=space (mirrors ubo.spaceBlend)

    // Current gravity "up" direction (points away from nearest planet).
    Vec3 gravityUp_{0.0f, 1.0f, 0.0f};
    // Pending gravity switch: accumulate time before committing to a face change.
    Vec3  pendingGravityUp_{0.0f, 1.0f, 0.0f};
    float gravityChangeTimer_ = 0.0f; // seconds the pending direction has been stable
    float verticalVelocity_ = 0.0f;
    float warpUntil_   = 0.0f; // game time until which gravity is suspended (post-teleport)
    bool  flyModeActive_ = false; // mirrors local flyMode flag for scroll callback access
    int   flySpeedLevel_ = 0;     // 0..3: 30, 150, 600, 2500 blocks/s
    Vec3  flyVelocity_{0.0f, 0.0f, 0.0f}; // fly mode inertia velocity
    float airPercent_ = 100.0f; // 0-100 remaining breath; depletes underwater

    // Mouse inertia: pending raw delta accumulated in mouseCallback, applied each frame
    double pendingMouseDX_ = 0.0;
    double pendingMouseDY_ = 0.0;
    // Inertia: residual angular velocity that decays over time (degrees)
    double mouseInertiaX_ = 0.0;
    double mouseInertiaY_ = 0.0;

    bool  leftMousePressedLast_  = false;
    bool  rightMousePressedLast_ = false;
    float leftHoldTimer_         = 0.0f; // seconds since last break action
    float rightHoldTimer_        = 0.0f; // seconds since last place action

    RaycastHit currentHit_{};
    RaycastHit previousHit_{};

    ChunkPos currentPlayerChunk_{999999, 999999, 999999};

    // Legacy creative-mode field kept for voxel mode; inventory drives normal play.
    BlockType selectedBlockType_ = BlockType::Grass;
    bool voxelEditMode_ = false;

    // ── Game state machine ─────────────────────────────────────────────────
    enum class GameState { MainMenu, Playing, Paused, Quitting };
    GameState gameState_ = GameState::MainMenu;

    // ── Inventory (4×4 grid; bottom row = hotbar) ──────────────────────────
    struct InvSlot { BlockType type = BlockType::Air; int count = 0; };
    InvSlot   inventory_[16] = {};
    InvSlot   heldItem_      = {};   // item held by cursor inside open inventory
    int       hotbarSlot_    = 0;    // 0-3
    bool      inventoryOpen_ = false;

    // Track E key for toggle
    bool eWasDown_ = false;

    // ── World / save system ────────────────────────────────────────────────
    std::string        activeWorldName_;   // empty = no world active
    std::vector<WorldInfo> worldList_;     // refreshed in main menu

    // ── Main-menu text input ───────────────────────────────────────────────
    // menuSubState: 0=world list, 1=create form, 2=world settings
    int         menuSubState_ = 0;
    int         menuSel_      = 0;   // selected world index
    std::string nameInput_;          // world name being typed (also rename buffer)
    std::string seedInput_;          // hex seed being typed (optional)
    int         textField_    = 0;   // 0=name, 1=seed
    float       cursorBlinkTimer_ = 0.0f;
    int         cursorBlink_      = 0;
    // World settings (menuSubState==2)
    int         settingsWorldSel_ = -1; // which world is being edited
    int         deleteConfirm_    = 0;  // 0=normal, 1=confirming delete

    // Leaf decay system
    struct LeafDecayItem { int x, y, z; float nextCheck; };
    std::deque<LeafDecayItem> leafDecayQueue_;
    // Prevent duplicate entries
    struct LeafDecayHash {
        std::size_t operator()(const std::tuple<int,int,int>& t) const noexcept {
            auto [x,y,z] = t;
            return std::size_t(x*73856093) ^ std::size_t(y*19349663) ^ std::size_t(z*83492791);
        }
    };
    std::unordered_set<std::tuple<int,int,int>, LeafDecayHash> leafDecaySet_;

    std::deque<ChunkPos> pendingChunkLoads_;
    std::unordered_set<ChunkPos, ChunkPosQueueHash> pendingChunkLoadSet_;
    std::vector<ChunkPos> desiredChunkPositions_;        // sorted by distance (for enqueueing)
    std::unordered_set<ChunkPos, ChunkPosQueueHash> desiredChunkSet_; // O(1) lookup

    // Deferred GPU buffer deletion — avoids vkDeviceWaitIdle in hot paths.
    // Buffers queued at frame N are deleted at frame N+MAX_FRAMES_IN_FLIGHT
    // (after their fence fires), so the GPU is guaranteed done with them.
    struct PendingDelete { VkBuffer buf; VkDeviceMemory mem; };
    std::array<std::vector<PendingDelete>, MAX_FRAMES_IN_FLIGHT> deletionQueues_;

    // ---- Background chunk generation ----------------------------------------
    struct ReadyChunk { ChunkPos pos; Chunk chunk; };

    std::mutex              genMutex_;
    std::condition_variable genCv_;
    std::deque<ChunkPos>    genJobQueue_;           // main→workers
    std::atomic<bool>       genRunning_{false};

    std::mutex              readyMutex_;
    std::deque<ReadyChunk>  readyChunks_;           // workers→main

    std::vector<std::thread> genWorkers_;

    VkInstance instance_ = VK_NULL_HANDLE;
    VkSurfaceKHR surface_ = VK_NULL_HANDLE;
    VkPhysicalDevice physicalDevice_ = VK_NULL_HANDLE;
    VkDevice device_ = VK_NULL_HANDLE;

    VkQueue graphicsQueue_ = VK_NULL_HANDLE;
    VkQueue presentQueue_ = VK_NULL_HANDLE;

    VkSwapchainKHR swapchain_ = VK_NULL_HANDLE;
    std::vector<VkImage> swapchainImages_;
    VkFormat swapchainImageFormat_{};
    VkExtent2D swapchainExtent_{};
    std::vector<VkImageView> swapchainImageViews_;

    VkRenderPass renderPass_ = VK_NULL_HANDLE;
    VkDescriptorSetLayout descriptorSetLayout_ = VK_NULL_HANDLE;
    VkPipelineLayout pipelineLayout_ = VK_NULL_HANDLE;
    VkPipeline graphicsPipeline_ = VK_NULL_HANDLE;
    VkPipeline debugLinePipeline_ = VK_NULL_HANDLE;
    std::vector<VkFramebuffer> swapchainFramebuffers_;

    VkCommandPool commandPool_ = VK_NULL_HANDLE;
    std::vector<VkCommandBuffer> commandBuffers_;

    std::unordered_map<ChunkPos, ChunkGpuMesh, ChunkPosQueueHash> chunkMeshes_;
    DebugGpuMesh outlineMesh_{};
    DebugGpuMesh breakMesh_{};

    std::vector<VkBuffer> uniformBuffers_;
    std::vector<VkDeviceMemory> uniformBuffersMemory_;

    VkDescriptorPool descriptorPool_ = VK_NULL_HANDLE;
    std::vector<VkDescriptorSet> descriptorSets_;

    VkImage depthImage_ = VK_NULL_HANDLE;
    VkDeviceMemory depthImageMemory_ = VK_NULL_HANDLE;
    VkImageView depthImageView_ = VK_NULL_HANDLE;
    VkFormat depthFormat_{};

    VkImage textureAtlasImage_ = VK_NULL_HANDLE;
    VkDeviceMemory textureAtlasImageMemory_ = VK_NULL_HANDLE;
    VkImageView textureAtlasImageView_ = VK_NULL_HANDLE;
    VkSampler textureAtlasSampler_ = VK_NULL_HANDLE;

    std::array<VkSemaphore, MAX_FRAMES_IN_FLIGHT> imageAvailableSemaphores_{};
    std::array<VkSemaphore, MAX_FRAMES_IN_FLIGHT> renderFinishedSemaphores_{};
    std::array<VkFence, MAX_FRAMES_IN_FLIGHT> inFlightFences_{};
    uint32_t currentFrame_ = 0;

    // Crosshair
    VkBuffer crosshairVertexBuffer_ = VK_NULL_HANDLE;
    VkDeviceMemory crosshairVertexBufferMemory_ = VK_NULL_HANDLE;
    VkBuffer crosshairIndexBuffer_ = VK_NULL_HANDLE;
    VkDeviceMemory crosshairIndexBufferMemory_ = VK_NULL_HANDLE;

    // Sky planet billboards (host-visible, rebuilt every frame)
    VkBuffer skyPlanetVertexBuffer_ = VK_NULL_HANDLE;
    VkDeviceMemory skyPlanetVertexMemory_ = VK_NULL_HANDLE;
    VkBuffer skyPlanetIndexBuffer_ = VK_NULL_HANDLE;
    VkDeviceMemory skyPlanetIndexMemory_ = VK_NULL_HANDLE;
    int skyPlanetIndexCount_ = 0;

    // Nearby planets list (rebuilt in updateSkyPlanetMesh, read by teleport)
    struct SkyPlanetEntry { Planet planet; float dist; };
    std::vector<SkyPlanetEntry> nearbyPlanets_;
    bool hasPlanetAim_ = false;   // crosshair is on a planet disc
    int  aimedPlanetIdx_ = -1;    // index into nearbyPlanets_

    // Star field (host-visible, rebuilt every frame with player position offset)
    struct StarDef { float dx, dy, dz; float size; float r, g, b; };
    std::vector<StarDef> starDefs_; // generated once at init
    VkBuffer      starVertexBuffer_ = VK_NULL_HANDLE;
    VkDeviceMemory starVertexMemory_ = VK_NULL_HANDLE;
    VkBuffer      starIndexBuffer_  = VK_NULL_HANDLE;
    VkDeviceMemory starIndexMemory_ = VK_NULL_HANDLE;
    int starIndexCount_ = 0;

    // Cloud layer (host-visible, rebuilt every frame, flat XZ quads at sky altitude)
    static constexpr int N_CLOUDS = 60;
    struct CloudDef {
        float ox, oz;   // world-relative XZ offset from player
        float w, d;     // width (X) and depth (Z)
        float alt;      // altitude offset above player eye
        float band;     // 0=low cumulus, 0.5=mid cumulus, 1=high cirrus
    };
    std::vector<CloudDef> cloudDefs_;         // generated once in createCloudBuffers()
    VkBuffer      cloudVertexBuffer_ = VK_NULL_HANDLE;
    VkDeviceMemory cloudVertexMemory_ = VK_NULL_HANDLE;
    VkBuffer      cloudIndexBuffer_  = VK_NULL_HANDLE;
    VkDeviceMemory cloudIndexMemory_ = VK_NULL_HANDLE;
    int cloudIndexCount_ = 0;


    float lastMvp_[16] = {
        1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1
    };
    float skyR_ = 0.30f, skyG_ = 0.58f, skyB_ = 0.97f; // updated each frame
    // Normalized sun direction, updated each frame from day-time
    float sunDirX_ = 0.000f, sunDirY_ = 0.944f, sunDirZ_ = 0.330f;
    float dayTime_  = 0.48f; // 0=midnight, 0.25=dawn, 0.5=noon, 0.75=dusk

    // ── Mob system ────────────────────────────────────────────────────────────
    static constexpr int N_MOBS_SHEEP = 22;
    static constexpr int N_MOBS_WOLF  = 8;
    static constexpr int N_MOBS_HUMAN = 14;

    std::vector<Mob> mobs_;
    bool  wolfKilledPlayer_ = false;
    float playerHp_         = 16.0f;   // current player HP (max 16)
    float attackCooldown_   = 0.0f;    // seconds until player can hit again

    // Block breaking
    int   breakX_ = 0, breakY_ = 0, breakZ_ = 0;
    float breakProgress_  = 0.0f;
    float footstepAccum_  = 0.0f;  // accumulated walk distance for footstep trigger

    Audio audio_;

    // Crafting grid (3x3 = 9 slots + 1 output)
    InvSlot craftGrid_[9] = {};
    InvSlot craftOutput_  = {};

    // Dropped items
    struct DroppedItem {
        double x, y, z;
        float  velGrav = 0.0f;
        BlockType type = BlockType::Air;
        int    count   = 1;
        float  lifetime = 300.0f;
        float  animTime = 0.0f;
    };
    std::vector<DroppedItem> drops_;

    // Drop GPU buffers (shares mob pipeline)
    VkBuffer       dropVertexBuffer_ = VK_NULL_HANDLE;
    VkDeviceMemory dropVertexMemory_ = VK_NULL_HANDLE;
    VkBuffer       dropIndexBuffer_  = VK_NULL_HANDLE;
    VkDeviceMemory dropIndexMemory_  = VK_NULL_HANDLE;
    int dropIndexCount_ = 0;

    // Mob rendering buffers (dynamic, rebuilt each frame)
    VkBuffer       mobVertexBuffer_ = VK_NULL_HANDLE;
    VkDeviceMemory mobVertexMemory_ = VK_NULL_HANDLE;
    VkBuffer       mobIndexBuffer_  = VK_NULL_HANDLE;
    VkDeviceMemory mobIndexMemory_  = VK_NULL_HANDLE;
    int mobDrawIndexCount_ = 0;

    // City building — shared state for all humans
    int  cityOriginX_ = 0, cityOriginY_ = 0, cityOriginZ_ = 0;
    int  cityNextBlock_ = 0;   // shared block index; each human claims next index
    bool cityReady_     = false;
    bool cityDone_      = false; // all blocks placed — switch to post-build AI
    int  cityUpAxis_    = 1;   // 0=X, 1=Y, 2=Z — which world axis is "up" for the city
    int  cityUpSign_    = 1;   // +1 or -1
    int  cityH1Axis_    = 0;   // first horizontal axis (city "column" direction)
    int  cityH2Axis_    = 2;   // second horizontal axis (city "row" direction)
    // World position of the single gate (front-wall centre, 1 block outside)
    float cityGateX_ = 0.0f, cityGateY_ = 0.0f, cityGateZ_ = 0.0f;
    // Convert local castle coords → world coords
    void cityLW(int lc, int lh, int lr, int& bx, int& by, int& bz) const {
        int w[3];
        const int origin[3] = {cityOriginX_, cityOriginY_, cityOriginZ_};
        w[cityH1Axis_] = origin[cityH1Axis_] + lc;
        w[cityUpAxis_] = origin[cityUpAxis_] + lh * cityUpSign_;
        w[cityH2Axis_] = origin[cityH2Axis_] + lr;
        bx = w[0]; by = w[1]; bz = w[2];
    }

    std::chrono::steady_clock::time_point startTime_{};
    std::chrono::steady_clock::time_point lastFrameTime_{};
    float currentTime_    = 0.0f; // seconds since startup (for animations)
    float worldPlayTime_  = 0.0f; // seconds spent in the active world (persisted)
    // Camera velocity (blocks/sec), updated each frame for odometer animation gating
    float camVelX_ = 0.0f, camVelY_ = 0.0f, camVelZ_ = 0.0f;
    double prevCamX_ = 0.0, prevCamY_ = 0.0, prevCamZ_ = 0.0;

    void initWindow();
    void initVulkan();
    void mainLoop();
    void cleanup();
    void drawFrame();
    void recreateSwapchain();
    void updateCamera(float dt);
    void handleBlockInteraction(float dt);
    void updateSelectionOutline();
    void updateChunkStreaming();
    void rebuildDirtyWorldChunks();
    void buildDesiredChunkSet(const ChunkPos& center);
    void enqueueMissingChunks();
    void processChunkLoadQueue();
    void processChunkUnloadQueue();
    bool isDesiredChunk(const ChunkPos& pos) const;
    void updateHotbarSelection();
    void updateWindowTitle();
    void workerLoop();
    void drainReadyChunks();
    void tickLeafDecay();
    void enqueueLeafDecayAround(int worldX, int worldY, int worldZ);

    int  getLodLevel(const ChunkPos& pos) const;
    void rebuildChunkMesh(const ChunkPos& pos);
    void rebuildChunksAroundBlock(int worldX, int worldY, int worldZ);

    static void framebufferResizeCallback(GLFWwindow* window, int width, int height);
    static void mouseCallback(GLFWwindow* window, double xpos, double ypos);
    static void scrollCallback(GLFWwindow* window, double xoffset, double yoffset);
    static void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods);
    static void charCallback(GLFWwindow* window, unsigned int codepoint);

    // World lifecycle
    void startWorld(const WorldInfo& info);
    void leaveWorld();               // save position, clear chunks, go to main menu
    void placePlayerAtSpawn(uint64_t seed);

    // Inventory helpers
    void      addToInventory(BlockType type);
    bool      consumeHotbarSlot();   // remove 1 from active slot; false if empty
    BlockType activeHotbarType() const;
    void      handleInventoryClick();
    int       countInventory(BlockType t) const;

    // Per-frame state handlers
    void handleMainMenu(float dt);
    void handlePaused(float dt);
    void handleQuitting(float dt);

    void createInstance();
    void createSurface();
    void pickPhysicalDevice();
    void createLogicalDevice();
    QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) const;
    SwapchainSupportDetails querySwapchainSupport(VkPhysicalDevice device) const;
    bool isDeviceSuitable(VkPhysicalDevice device) const;
    uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) const;
    VkFormat findSupportedFormat(
        const std::vector<VkFormat>& candidates,
        VkImageTiling tiling,
        VkFormatFeatureFlags features
    ) const;
    VkFormat findDepthFormat() const;
    bool hasStencilComponent(VkFormat format) const;

    void createSwapchain();
    void createImageViews();
    void createDepthResources();
    void createFramebuffers();
    void cleanupSwapchain();
    VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& formats) const;
    VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& presentModes) const;
    VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) const;
    void createImage(
        uint32_t width,
        uint32_t height,
        VkFormat format,
        VkImageTiling tiling,
        VkImageUsageFlags usage,
        VkMemoryPropertyFlags properties,
        VkImage& image,
        VkDeviceMemory& imageMemory
    );
    VkImageView createImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags) const;

    void createRenderPass();
    void createDescriptorSetLayout();
    void createGraphicsPipeline();
    static std::vector<char> readFile(const std::string& filename);
    VkShaderModule createShaderModule(const std::vector<char>& code) const;

    void createCommandPool();
    void createUniformBuffers();
    void createDescriptorPool();
    void createDescriptorSets();
    void createCommandBuffers();
    void createSyncObjects();
    void createBuffer(
        VkDeviceSize size,
        VkBufferUsageFlags usage,
        VkMemoryPropertyFlags properties,
        VkBuffer& buffer,
        VkDeviceMemory& bufferMemory
    );
    void destroyBuffer(VkBuffer& buffer, VkDeviceMemory& memory);
    void destroyBufferDeferred(VkBuffer& buffer, VkDeviceMemory& memory);
    void createChunkMeshes();
    void destroyChunkMeshes();
    void rebuildOutlineMesh();
    void destroyOutlineMesh();
    void rebuildBreakMesh();
    void destroyBreakMesh();
    void createCrosshairMesh();
    void destroyCrosshairMesh();
    void createSkyPlanetBuffers();
    void destroySkyPlanetBuffers();
    void updateSkyPlanetMesh();
    void createStarBuffers();
    void destroyStarBuffers();
    void updateStarMesh();
    void createCloudBuffers();
    void destroyCloudBuffers();
    void updateCloudMesh();

    // Block breaking + drops + crafting
    void spawnDrop(double x, double y, double z, BlockType type, int count=1);
    void updateDrops(float dt);
    void updateCraftOutput();
    void createDropBuffers();
    void destroyDropBuffers();
    void updateDropMesh();

    // Mob system
    void spawnMobs();
    void updateMobs(float dt);
    void updateSheep(Mob& mob, float dt);
    void updateWolf(Mob& mob, float dt);
    void updateHuman(Mob& mob, float dt);
    bool mobMoveTo(Mob& mob, float tx, float ty, float tz, float speed, float dt);
    void mobApplyPhysics(Mob& mob, float dt);
    int  nearestMobIdx(const Mob& mob, MobType type, float maxDist) const;
    float distToPlayer(const Mob& mob) const;
    bool cityGetBlock(int step, int& bx, int& by, int& bz, BlockType& type) const;
    void createMobBuffers();
    void destroyMobBuffers();
    void updateMobMesh();

    static void mat4Inverse(const float in[16], float out[16]);
    void updateUniformBuffer(uint32_t imageIndex);
    void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex);

    void createTextureAtlasResources();
    void destroyTextureAtlasResources();
    VkCommandBuffer beginSingleTimeCommands();
    void endSingleTimeCommands(VkCommandBuffer commandBuffer);
    void transitionImageLayout(
        VkImage image,
        VkFormat format,
        VkImageLayout oldLayout,
        VkImageLayout newLayout
    );
    void copyBufferToImage(
        VkBuffer buffer,
        VkImage image,
        uint32_t width,
        uint32_t height
    );
};