#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"

static constexpr float SKY_PLANET_DIST   = 900.0f; // blocks from player
static constexpr float SKY_PLANET_SCALE  = 10.0f;  // visual magnification

static constexpr float BIOME_COLORS[7][3] = {
    {0.35f, 0.70f, 0.25f},  // Temperate  – green
    {0.90f, 0.72f, 0.30f},  // Desert     – sandy
    {0.85f, 0.92f, 1.00f},  // Snow       – icy blue-white
    {0.20f, 0.45f, 0.90f},  // Ocean      – blue
    {0.30f, 0.50f, 0.20f},  // Swamp      – murky green
    {0.70f, 0.25f, 0.10f},  // Volcanic   – dark red
    {0.15f, 0.55f, 0.15f},  // Forest     – dark green
};
static constexpr int  MAX_SKY_PLANETS    = 32; // plenty of room for planets + sun + moon
static constexpr float SKY_PLANET_TEX_ID = -1.25f; // sentinel (hits MVP path)

void App::createSkyPlanetBuffers() {
    // Allocate host-visible buffers for max 8 planet quads (4 verts, 6 indices each).
    struct V { float pos[3]; float col[3]; float uv[2]; float ti; };
    const VkDeviceSize vsize = MAX_SKY_PLANETS * 4 * sizeof(V);
    const VkDeviceSize isize = MAX_SKY_PLANETS * 6 * sizeof(uint32_t);
    createBuffer(vsize,
        VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
        VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
        skyPlanetVertexBuffer_, skyPlanetVertexMemory_);
    createBuffer(isize,
        VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
        VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
        skyPlanetIndexBuffer_, skyPlanetIndexMemory_);
}

void App::destroySkyPlanetBuffers() {
    destroyBuffer(skyPlanetVertexBuffer_, skyPlanetVertexMemory_);
    destroyBuffer(skyPlanetIndexBuffer_,  skyPlanetIndexMemory_);
}

void App::updateSkyPlanetMesh() {
    struct V { float pos[3]; float col[3]; float uv[2]; float ti; };

    const float px = camera_.position.x;
    const float py = camera_.position.y;
    const float pz = camera_.position.z;

    const Vec3 camRight   = camera_.right();
    const Vec3 camUp      = camera_.up();
    const Vec3 camForward = camera_.forward();

    // Gather nearby planets from ±3 sectors, sorted by distance.
    // Exclude only planets whose surface the player is currently inside.
    struct Cand { Planet p; float dist; };
    std::vector<Cand> cands;
    const int64_t SZ  = SECTOR_SIZE;
    const int64_t scx = static_cast<int64_t>(std::floor(px / SZ));
    const int64_t scy = static_cast<int64_t>(std::floor(py / SZ));
    const int64_t scz = static_cast<int64_t>(std::floor(pz / SZ));
    for (int64_t sx = scx - 5; sx <= scx + 5; ++sx)
    for (int64_t sy = scy - 5; sy <= scy + 5; ++sy)
    for (int64_t sz = scz - 5; sz <= scz + 5; ++sz) {
        Planet p;
        if (!PlanetManager::sectorPlanet(sx, sy, sz, p)) continue;
        float dx = p.cx - px, dy = p.cy - py, dz = p.cz - pz;
        float dist = std::sqrt(dx*dx + dy*dy + dz*dz);
        // Only hide a planet when the player is actually on its surface
        if (dist < static_cast<float>(p.baseRadius) * 1.2f) continue;
        Cand cand; cand.p = p; cand.dist = dist;
        cands.push_back(cand);
    }
    std::sort(cands.begin(), cands.end(), [](const Cand& a, const Cand& b){ return a.dist < b.dist; });
    if ((int)cands.size() > MAX_SKY_PLANETS - 3) cands.resize(MAX_SKY_PLANETS - 3);

    // Populate nearbyPlanets_ (used by teleport logic)
    nearbyPlanets_.clear();
    for (const auto& c : cands) {
        SkyPlanetEntry e; e.planet = c.p; e.dist = c.dist;
        nearbyPlanets_.push_back(e);
    }

    // Detect which planet the crosshair is aimed at
    // (dot product of camera forward with planet direction > cos(4°) ≈ 0.9976)
    hasPlanetAim_ = false;
    aimedPlanetIdx_ = -1;
    float bestDot = 0.9976f; // cos(4°)
    for (int i = 0; i < (int)nearbyPlanets_.size(); ++i) {
        const Planet& p = nearbyPlanets_[i].planet;
        float d = nearbyPlanets_[i].dist;
        if (d < 1.0f) continue;
        float ndx = (p.cx - px) / d, ndy = (p.cy - py) / d, ndz = (p.cz - pz) / d;
        float dot = ndx * camForward.x + ndy * camForward.y + ndz * camForward.z;
        if (dot > bestDot) { bestDot = dot; aimedPlanetIdx_ = i; hasPlanetAim_ = true; }
    }

    // Build billboard quads
    std::vector<V>        verts;
    std::vector<uint32_t> indices;
    verts.reserve((cands.size() + 2) * 4);
    indices.reserve((cands.size() + 2) * 6);

    // ── Sun: two overlapping camera-facing billboards ──────────────────────
    // Core (sharp disc, warm white) + outer glow (larger, soft orange corona).
    // Uses texIndex = -5.0, handled by the sun branch in triangle.frag.
    {
        // Sun direction from day-time state
        const float SUN_DX = sunDirX_;
        const float SUN_DY = sunDirY_;
        const float SUN_DZ = sunDirZ_;
        const float sx = px + SUN_DX * SKY_PLANET_DIST;
        const float sy = py + SUN_DY * SKY_PLANET_DIST;
        const float sz = pz + SUN_DZ * SKY_PLANET_DIST;
        const float offR[4] = {-1, 1, 1, -1};
        const float offU[4] = { 1, 1,-1, -1};
        const float uvX[4]  = { 0, 1, 1,  0};
        const float uvY[4]  = { 0, 0, 1,  1};

        // Dawn/dusk warmth blend for sun colour
        float dawnF = std::pow(std::max(0.0f, 1.0f - std::abs(SUN_DY * 3.5f)), 2.0f);
        float sunR  = 1.00f;
        float sunG  = 0.97f - dawnF * 0.32f; // goes orange at dawn/dusk
        float sunB  = 0.75f - dawnF * 0.55f;
        float coroR = 1.00f;
        float coroG = 0.65f - dawnF * 0.30f;
        float coroB = 0.20f - dawnF * 0.18f;

        // Only draw sun when it is above the horizon
        if (SUN_DY > -0.10f) {
            // Glow quad (soft corona)
            {
                const float hs = 115.0f;
                const uint32_t base = static_cast<uint32_t>(verts.size());
                for (int i = 0; i < 4; ++i) {
                    V v{};
                    v.pos[0] = sx + camRight.x*hs*offR[i] + camUp.x*hs*offU[i];
                    v.pos[1] = sy + camRight.y*hs*offR[i] + camUp.y*hs*offU[i];
                    v.pos[2] = sz + camRight.z*hs*offR[i] + camUp.z*hs*offU[i];
                    v.col[0] = coroR; v.col[1] = coroG; v.col[2] = coroB;
                    v.uv[0] = uvX[i]; v.uv[1] = uvY[i];
                    v.ti = -5.0f;
                    verts.push_back(v);
                }
                indices.insert(indices.end(), {base, base+1, base+2, base, base+2, base+3});
            }
            // Core quad
            {
                const float hs = 36.0f;
                const uint32_t base = static_cast<uint32_t>(verts.size());
                for (int i = 0; i < 4; ++i) {
                    V v{};
                    v.pos[0] = sx + camRight.x*hs*offR[i] + camUp.x*hs*offU[i];
                    v.pos[1] = sy + camRight.y*hs*offR[i] + camUp.y*hs*offU[i];
                    v.pos[2] = sz + camRight.z*hs*offR[i] + camUp.z*hs*offU[i];
                    v.col[0] = sunR; v.col[1] = sunG; v.col[2] = sunB;
                    v.uv[0] = uvX[i]; v.uv[1] = uvY[i];
                    v.ti = -5.0f;
                    verts.push_back(v);
                }
                indices.insert(indices.end(), {base, base+1, base+2, base, base+2, base+3});
            }
        }

        // Moon — opposite direction from sun; visible when sun is below horizon
        if (-SUN_DY > -0.10f) {
            const float mx = px - SUN_DX * SKY_PLANET_DIST;
            const float my = py - SUN_DY * SKY_PLANET_DIST;
            const float mz = pz - SUN_DZ * SKY_PLANET_DIST;
            // Core
            const float hs = 22.0f;
            const uint32_t base = static_cast<uint32_t>(verts.size());
            for (int i = 0; i < 4; ++i) {
                V v{};
                v.pos[0] = mx + camRight.x*hs*offR[i] + camUp.x*hs*offU[i];
                v.pos[1] = my + camRight.y*hs*offR[i] + camUp.y*hs*offU[i];
                v.pos[2] = mz + camRight.z*hs*offR[i] + camUp.z*hs*offU[i];
                v.col[0] = 0.90f; v.col[1] = 0.92f; v.col[2] = 1.00f; // silver-white
                v.uv[0] = uvX[i]; v.uv[1] = uvY[i];
                v.ti = -5.0f;
                verts.push_back(v);
            }
            indices.insert(indices.end(), {base, base+1, base+2, base, base+2, base+3});
        }
    }

    for (int ci = 0; ci < (int)cands.size(); ++ci) {
        const Planet& p = cands[ci].p;
        float dist = cands[ci].dist;
        if (dist < 1.0f) continue;

        float dx = p.cx - px, dy = p.cy - py, dz = p.cz - pz;
        float inv = 1.0f / dist;
        float ndx = dx * inv, ndy = dy * inv, ndz = dz * inv;

        float cx2 = px + ndx * SKY_PLANET_DIST;
        float cy2 = py + ndy * SKY_PLANET_DIST;
        float cz2 = pz + ndz * SKY_PLANET_DIST;

        // Physical angular size: α = 2·arctan(R/d).
        // Billboard at SKY_PLANET_DIST subtends the same angle when
        // halfSize = SKY_PLANET_DIST · tan(arctan(R/d)) = SKY_PLANET_DIST · R/d.
        const float halfAngle = std::atan(static_cast<float>(p.baseRadius) / dist);
        const float halfSize  = SKY_PLANET_DIST * std::tan(halfAngle);

        // High-quality per-planet hash so same-biome planets look different.
        // Uses a multiplicative xorshift so even adjacent planets (cx±1) scatter widely.
        auto phash = [](int32_t a, int32_t b, int32_t c) -> float {
            uint32_t h = static_cast<uint32_t>(a * 1619 + b * 31337 + c * 6271 + 1013904223);
            h ^= (h >> 17); h *= 0xBF58476Du;
            h ^= (h >> 31); h *= 0x94D049BBu;
            h ^= (h >> 27);
            return static_cast<float>(h & 0xFFFFFFu) / float(0xFFFFFFu);
        };
        float seedX = phash(p.cx, p.cy, p.cz);
        float seedY = phash(p.cx + 7919, p.cz - 3571, p.cy + 12345);
        // Biome packed into blue channel: (biome + 0.5) / 7  → 0.07 .. 0.93
        float biomeF = (static_cast<int>(p.biome) + 0.5f) / 7.0f;

        uint32_t base = static_cast<uint32_t>(verts.size());
        const float offR[4] = {-1, 1, 1, -1};
        const float offU[4] = { 1, 1,-1, -1};
        const float uvX[4]  = { 0, 1, 1,  0};
        const float uvY[4]  = { 0, 0, 1,  1};
        for (int i = 0; i < 4; ++i) {
            V v{};
            v.pos[0] = cx2 + camRight.x * halfSize * offR[i] + camUp.x * halfSize * offU[i];
            v.pos[1] = cy2 + camRight.y * halfSize * offR[i] + camUp.y * halfSize * offU[i];
            v.pos[2] = cz2 + camRight.z * halfSize * offR[i] + camUp.z * halfSize * offU[i];
            v.col[0] = seedX;   // unique noise seed
            v.col[1] = seedY;
            v.col[2] = biomeF;  // biome index (decoded in frag shader)
            v.uv[0]  = uvX[i]; v.uv[1] = uvY[i];
            v.ti     = SKY_PLANET_TEX_ID;
            verts.push_back(v);
        }
        indices.insert(indices.end(), {base, base+1, base+2, base, base+2, base+3});
    }

    skyPlanetIndexCount_ = static_cast<int>(indices.size());
    if (skyPlanetIndexCount_ == 0 || verts.empty()) return;

    // Upload
    void* vdata = nullptr;
    vkMapMemory(device_, skyPlanetVertexMemory_, 0,
        verts.size() * sizeof(V), 0, &vdata);
    std::memcpy(vdata, verts.data(), verts.size() * sizeof(V));
    vkUnmapMemory(device_, skyPlanetVertexMemory_);

    void* idata = nullptr;
    vkMapMemory(device_, skyPlanetIndexMemory_, 0,
        indices.size() * sizeof(uint32_t), 0, &idata);
    std::memcpy(idata, indices.data(), indices.size() * sizeof(uint32_t));
    vkUnmapMemory(device_, skyPlanetIndexMemory_);
}

// ---- Star field ----------------------------------------------------------
static constexpr int   N_STARS       = 600;
static constexpr float SKY_STAR_DIST = 850.0f;
static constexpr float SKY_STAR_TI   = -0.75f; // hits fragment: < -0.625 check

void App::createStarBuffers() {
    // Generate star directions once using a simple xorshift RNG
    starDefs_.reserve(N_STARS);
    uint64_t rng = 0xDEADBEEFCAFEBABEULL;
    auto rnd = [&]() -> float {
        rng ^= rng << 13; rng ^= rng >> 7; rng ^= rng << 17;
        return static_cast<float>(rng & 0xFFFFFF) / static_cast<float>(0x1000000);
    };
    for (int i = 0; i < N_STARS; ++i) {
        // Uniform distribution on sphere via rejection
        float x, y, z, len;
        do {
            x = rnd() * 2.0f - 1.0f;
            y = rnd() * 2.0f - 1.0f;
            z = rnd() * 2.0f - 1.0f;
            len = std::sqrt(x*x + y*y + z*z);
        } while (len < 0.001f || len > 1.0f);
        StarDef s{};
        s.dx = x / len; s.dy = y / len; s.dz = z / len;
        // Size: 0.5 – 2.5 blocks half-side; larger = rarer, brighter star
        s.size = 0.5f + rnd() * rnd() * 2.0f; // skew toward small
        // Colour: mostly white, occasional warm/cool tint
        float tint = rnd();
        if      (tint < 0.12f) { s.r = 0.8f; s.g = 0.9f; s.b = 1.0f; }  // blue-white
        else if (tint < 0.22f) { s.r = 1.0f; s.g = 0.9f; s.b = 0.7f; }  // warm yellow
        else if (tint < 0.28f) { s.r = 1.0f; s.g = 0.7f; s.b = 0.5f; }  // orange
        else                   { s.r = 1.0f; s.g = 1.0f; s.b = 1.0f; }  // pure white
        starDefs_.push_back(s);
    }

    struct V { float pos[3]; float col[3]; float uv[2]; float ti; };
    createBuffer(N_STARS * 4 * sizeof(V),
        VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
        VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
        starVertexBuffer_, starVertexMemory_);
    createBuffer(N_STARS * 6 * sizeof(uint32_t),
        VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
        VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
        starIndexBuffer_, starIndexMemory_);

    // Index buffer is static — fill once
    std::vector<uint32_t> idx;
    idx.reserve(N_STARS * 6);
    for (int i = 0; i < N_STARS; ++i) {
        uint32_t b = static_cast<uint32_t>(i * 4);
        idx.insert(idx.end(), {b, b+1, b+2, b, b+2, b+3});
    }
    void* idata = nullptr;
    vkMapMemory(device_, starIndexMemory_, 0, idx.size() * sizeof(uint32_t), 0, &idata);
    std::memcpy(idata, idx.data(), idx.size() * sizeof(uint32_t));
    vkUnmapMemory(device_, starIndexMemory_);
    starIndexCount_ = static_cast<int>(idx.size());
}

void App::destroyStarBuffers() {
    destroyBuffer(starVertexBuffer_, starVertexMemory_);
    destroyBuffer(starIndexBuffer_,  starIndexMemory_);
}

// ── Cloud layer ────────────────────────────────────────────────────────────

void App::createCloudBuffers() {
    struct V { float pos[3]; float col[3]; float uv[2]; float ti; };
    const VkDeviceSize vsize = static_cast<VkDeviceSize>(N_CLOUDS * 4 * sizeof(V));
    const VkDeviceSize isize = static_cast<VkDeviceSize>(N_CLOUDS * 6 * sizeof(uint32_t));

    createBuffer(vsize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
        VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
        cloudVertexBuffer_, cloudVertexMemory_);
    createBuffer(isize, VK_BUFFER_USAGE_INDEX_BUFFER_BIT,
        VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
        cloudIndexBuffer_, cloudIndexMemory_);

    std::vector<uint32_t> idx;
    idx.reserve(N_CLOUDS * 6);
    for (int i = 0; i < N_CLOUDS; ++i) {
        const uint32_t b = static_cast<uint32_t>(i * 4);
        idx.insert(idx.end(), {b, b+1, b+2, b, b+2, b+3});
    }
    void* idata = nullptr;
    vkMapMemory(device_, cloudIndexMemory_, 0, isize, 0, &idata);
    std::memcpy(idata, idx.data(), isize);
    vkUnmapMemory(device_, cloudIndexMemory_);
    cloudIndexCount_ = static_cast<int>(idx.size());

    // Three altitude bands:
    //   0–24  (band 0.0): low cumulus    — large, 88% opaque
    //   25–44 (band 0.5): mid cumulus    — medium, 72% opaque
    //   45–59 (band 1.0): high cirrus    — thin, stretched, 30% opaque
    uint64_t rng = 0xDEADBEEF12345678ULL;
    auto rndF = [&]() -> float {
        rng ^= rng << 13; rng ^= rng >> 7; rng ^= rng << 17;
        return static_cast<float>(rng & 0xFFFF) / 65535.0f;
    };
    cloudDefs_.clear();
    cloudDefs_.reserve(N_CLOUDS);
    for (int i = 0; i < N_CLOUDS; ++i) {
        const float angle = rndF() * 6.2832f;
        CloudDef cd;
        if (i < 25) {
            // Low cumulus: large, close
            cd.band = 0.0f;
            cd.alt  = 48.0f + rndF() * 18.0f;
            float dist = 20.0f + rndF() * 160.0f;
            cd.ox = std::cos(angle) * dist;
            cd.oz = std::sin(angle) * dist;
            cd.w  = 45.0f + rndF() * 65.0f;
            cd.d  = 30.0f + rndF() * 45.0f;
        } else if (i < 45) {
            // Mid cumulus: medium
            cd.band = 0.5f;
            cd.alt  = 82.0f + rndF() * 20.0f;
            float dist = 15.0f + rndF() * 185.0f;
            cd.ox = std::cos(angle) * dist;
            cd.oz = std::sin(angle) * dist;
            cd.w  = 30.0f + rndF() * 50.0f;
            cd.d  = 20.0f + rndF() * 35.0f;
        } else {
            // High cirrus: wide, thin, wispy
            cd.band = 1.0f;
            cd.alt  = 120.0f + rndF() * 30.0f;
            float dist = 10.0f + rndF() * 200.0f;
            cd.ox = std::cos(angle) * dist;
            cd.oz = std::sin(angle) * dist;
            cd.w  = 80.0f + rndF() * 100.0f;  // very wide
            cd.d  = 12.0f + rndF() * 18.0f;   // very thin
        }
        cloudDefs_.push_back(cd);
    }
}

void App::destroyCloudBuffers() {
    destroyBuffer(cloudVertexBuffer_, cloudVertexMemory_);
    destroyBuffer(cloudIndexBuffer_,  cloudIndexMemory_);
}

// ── 3-D water bubbles ──────────────────────────────────────────────────────

void App::updateCloudMesh() {
    if (cloudDefs_.empty() || cloudVertexBuffer_ == VK_NULL_HANDLE) return;

    struct V { float pos[3]; float col[3]; float uv[2]; float ti; };
    constexpr float CLOUD_WRAP   = 200.0f;
    constexpr float WIND_R       = 2.8f;   // wind speed along "right" axis
    constexpr float WIND_F       = 1.0f;   // wind speed along "forward" axis
    constexpr float CLOUD_TEX_ID = -4.0f;

    static V verts[N_CLOUDS * 4];

    const float px = camera_.position.x;
    const float py = camera_.position.y;
    const float pz = camera_.position.z;
    const float W2 = CLOUD_WRAP * 2.0f;

    // Build gravity-relative orthonormal basis for the horizontal cloud plane.
    // Clouds always float perpendicular to gravityUp_, regardless of which face of
    // the planet the player is on.
    const Vec3 up = gravityUp_;
    const Vec3 wref = (std::abs(up.y) < 0.9f) ? Vec3{0.0f, 1.0f, 0.0f}
                                               : Vec3{1.0f, 0.0f, 0.0f};
    auto cross3 = [](Vec3 a, Vec3 b) -> Vec3 {
        return {a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x};
    };
    auto norm3 = [](Vec3 v) -> Vec3 {
        float len = std::sqrt(v.x*v.x + v.y*v.y + v.z*v.z);
        if (len < 1e-6f) return {1.0f, 0.0f, 0.0f};
        return {v.x/len, v.y/len, v.z/len};
    };
    const Vec3 right = norm3(cross3(wref, up));
    const Vec3 fwd   = norm3(cross3(up, right));

    // Project player position onto right/fwd axes for wrapping.
    const float pR = px*right.x + py*right.y + pz*right.z;
    const float pF = px*fwd.x   + py*fwd.y   + pz*fwd.z;

    auto wrapRel = [&](float raw) -> float {
        float r = std::fmod(raw + CLOUD_WRAP, W2);
        if (r < 0.0f) r += W2;
        return r - CLOUD_WRAP;
    };

    for (int i = 0; i < N_CLOUDS; ++i) {
        const CloudDef& cd = cloudDefs_[i];

        // cd.ox/oz are used as offsets along right/fwd axes (arbitrary names kept).
        const float relR = wrapRel(cd.ox + currentTime_ * WIND_R - pR);
        const float relF = wrapRel(cd.oz + currentTime_ * WIND_F - pF);

        // Cloud centre: player position + alt in gravity-up direction + lateral drift.
        const float cx = px + up.x*cd.alt + right.x*relR + fwd.x*relF;
        const float cy = py + up.y*cd.alt + right.y*relR + fwd.y*relF;
        const float cz = pz + up.z*cd.alt + right.z*relR + fwd.z*relF;

        const float hw = cd.w * 0.5f;
        const float hd = cd.d * 0.5f;

        // Deterministic per-cloud noise seed (stable across wraps)
        const float seedX = std::fmod(std::abs(cd.ox) * 0.0237f, 1.0f);
        const float seedZ = std::fmod(std::abs(cd.oz) * 0.0193f, 1.0f);

        // Quad corners: ±hw along right, ±hd along fwd.
        // UV layout: u=0,1,1,0  v=0,0,1,1
        const float offR[4] = {-1.0f, 1.0f, 1.0f,-1.0f};
        const float offF[4] = {-1.0f,-1.0f, 1.0f, 1.0f};
        const float us[4]   = { 0.0f, 1.0f, 1.0f, 0.0f};
        const float vs[4]   = { 0.0f, 0.0f, 1.0f, 1.0f};

        for (int k = 0; k < 4; ++k) {
            V& v = verts[i * 4 + k];
            const float dr = hw * offR[k];
            const float df = hd * offF[k];
            v.pos[0] = cx + right.x*dr + fwd.x*df;
            v.pos[1] = cy + right.y*dr + fwd.y*df;
            v.pos[2] = cz + right.z*dr + fwd.z*df;
            v.col[0] = seedX;
            v.col[1] = seedZ;
            v.col[2] = cd.band;
            v.uv[0]  = us[k];
            v.uv[1]  = vs[k];
            v.ti     = CLOUD_TEX_ID;
        }
    }

    const VkDeviceSize vsize = static_cast<VkDeviceSize>(N_CLOUDS * 4 * sizeof(V));
    void* vdata = nullptr;
    vkMapMemory(device_, cloudVertexMemory_, 0, vsize, 0, &vdata);
    std::memcpy(vdata, verts, static_cast<size_t>(vsize));
    vkUnmapMemory(device_, cloudVertexMemory_);
}

void App::updateStarMesh() {
    struct V { float pos[3]; float col[3]; float uv[2]; float ti; };

    const float px = camera_.position.x;
    const float py = camera_.position.y;
    const float pz = camera_.position.z;
    const Vec3 CR = camera_.right();
    const Vec3 CU = camera_.up();
    const Vec3 gu = camera_.worldUp;

    // Pack only visible stars contiguously; pre-built index buffer uses stride 4.
    static V verts[N_STARS * 4];
    const float offR[4] = {-1, 1, 1, -1};
    const float offU[4] = { 1, 1,-1, -1};
    const float uvX[4]  = { 0, 1, 1,  0};
    const float uvY[4]  = { 0, 0, 1,  1};

    int count = 0;
    for (int i = 0; i < N_STARS; ++i) {
        const StarDef& s = starDefs_[i];
        // On the surface: omit stars pointing underground (z-fight with terrain).
        // In space: show all stars (full sky sphere).
        if (spaceBlend_ < 0.5f) {
            float dotUp = s.dx * gu.x + s.dy * gu.y + s.dz * gu.z;
            if (dotUp < -0.34f) continue;
        }

        float cx = px + s.dx * SKY_STAR_DIST;
        float cy = py + s.dy * SKY_STAR_DIST;
        float cz = pz + s.dz * SKY_STAR_DIST;
        for (int k = 0; k < 4; ++k) {
            V& v = verts[count * 4 + k]; // packed into count slot, not i slot
            v.pos[0] = cx + CR.x * s.size * offR[k] + CU.x * s.size * offU[k];
            v.pos[1] = cy + CR.y * s.size * offR[k] + CU.y * s.size * offU[k];
            v.pos[2] = cz + CR.z * s.size * offR[k] + CU.z * s.size * offU[k];
            v.col[0] = s.r; v.col[1] = s.g; v.col[2] = s.b;
            v.uv[0]  = uvX[k]; v.uv[1] = uvY[k];
            v.ti     = SKY_STAR_TI;
        }
        ++count;
    }
    // Draw only the visible stars (index buffer was built for all N_STARS in order)
    starIndexCount_ = count * 6;
    if (count == 0) return;

    void* vdata = nullptr;
    vkMapMemory(device_, starVertexMemory_, 0, count * 4 * sizeof(V), 0, &vdata);
    std::memcpy(vdata, verts, count * 4 * sizeof(V));
    vkUnmapMemory(device_, starVertexMemory_);
}

