#include "TopingSystem.h"
#include "VoxelWorld.h"
#include "wiJobSystem.h"
#include <cmath>
#include <cstring>

namespace voxel {

static constexpr float PI = 3.14159265f;

// ── Edge definitions for +Y face ────────────────────────────────
// Each edge sits on one side of the unit square [0,1] (the XZ plane at y=1).
// The bevel strip runs along the edge, with a wedge cross-section
// rising from the voxel face (y=1) to a peak (y=1+h) and sloping
// inward by width w.
//
// Cross-section (looking along the strip):
//
//         peak (edge, 1+h)
//         /|
//        / |
//       /  |  slope face (visible from above)
//      /   |
//     /    |  outer wall (visible from the side)
//    /     |
//   inner  outer
//   (1-w,1) (edge,1)
//
struct EdgeDef {
    float sx, sz;    // strip start point (on the voxel face)
    float ex, ez;    // strip end point
    float ix, iz;    // inward direction (unit, perpendicular to strip)
    float nx, nz;    // outer wall normal (points outward)
};

static const EdgeDef kEdges[4] = {
    //  sx    sz    ex    ez    ix    iz    nx    nz
    { 1.0f, 0.0f, 1.0f, 1.0f,-1.0f, 0.0f, 1.0f, 0.0f }, // bit 0: +X edge
    { 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,-1.0f, 0.0f }, // bit 1: -X edge
    { 0.0f, 1.0f, 1.0f, 1.0f, 0.0f,-1.0f, 0.0f, 1.0f }, // bit 2: +Z edge
    { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,-1.0f }, // bit 3: -Z edge
};

// ── Corner definitions ───────────────────────────────────────────
// Each corner of the +Y face is shared by two edges. When BOTH edges
// are open (bits unset), a corner fill triangle closes the gap between
// the two bevel strips. When only one edge is open, a cap triangle
// closes the strip end.
struct CornerDef {
    float cx, cz;      // corner position in [0,1]^2
    int   bitA, bitB;  // edge bits that share this corner
};

static const CornerDef kCorners[4] = {
    { 1.0f, 0.0f, 0, 3 },  // +X start / -Z end
    { 1.0f, 1.0f, 0, 2 },  // +X end   / +Z end
    { 0.0f, 1.0f, 1, 2 },  // -X end   / +Z start
    { 0.0f, 0.0f, 1, 3 },  // -X start / -Z start
};

// For each edge: which other edge shares its start/end corner
static const int kStartNeighbor[4] = { 3, 3, 1, 1 };
static const int kEndNeighbor[4]   = { 2, 2, 0, 0 };

// ── Helper: emit one triangle with auto-corrected winding ───────
// Compares the geometric normal (from vertex cross product) to the
// desired shading normal. If they disagree, swaps B and C to flip
// the winding so the triangle is front-facing (CW in Wicked Engine).
static void emitTri(std::vector<TopingVertex>& v,
    float ax, float ay, float az,
    float bx, float by, float bz,
    float cx, float cy, float cz,
    float nx, float ny, float nz)
{
    // Geometric normal = cross(AB, AC)
    float abx = bx - ax, aby = by - ay, abz = bz - az;
    float acx = cx - ax, acy = cy - ay, acz = cz - az;
    float gnx = aby * acz - abz * acy;
    float gny = abz * acx - abx * acz;
    float gnz = abx * acy - aby * acx;

    // If geometric normal disagrees with desired normal, swap B<->C
    if (gnx * nx + gny * ny + gnz * nz > 0.0f) {
        v.push_back({ ax, ay, az, nx, ny, nz });
        v.push_back({ cx, cy, cz, nx, ny, nz });
        v.push_back({ bx, by, bz, nx, ny, nz });
    } else {
        v.push_back({ ax, ay, az, nx, ny, nz });
        v.push_back({ bx, by, bz, nx, ny, nz });
        v.push_back({ cx, cy, cz, nx, ny, nz });
    }
}

// ── Deterministic hash for pseudo-random blade variety ───────────
// Returns a value in [0,1) from integer inputs. Golden ratio based.
static float hashF(int a, int b, int c) {
    float x = (float)(a * 127 + b * 311 + c * 523) * 0.6180339887f;
    return x - floorf(x);
}

// ── Emit a single grass blade (3 segments, double-sided) ────────
// The blade is a wide, leaf-shaped ribbon with rounded tip.
// 4 cross-sections: wide base → widest at 1/3 height → taper → rounded tip.
// Double-sided for backface-culled PSO.
//
// midLeanRatio controls the curvature profile:
//   low  (0.05-0.15): mostly straight bottom, sharp curve at top
//   high (0.30-0.50): curves early from base, droopy look
//
static void emitBlade(std::vector<TopingVertex>& verts,
                       float px, float pz,        // base position on voxel face (y=1)
                       float outX, float outZ,    // outward direction from edge (normalized)
                       float height, float baseW,
                       float lean, float angleDeg,
                       float midLeanRatio)         // 0.0-0.5, curvature shape
{
    // Rotate outward direction by angle to get lean direction
    float angleRad = angleDeg * PI / 180.0f;
    float ca = cosf(angleRad), sa = sinf(angleRad);
    float lx = outX * ca - outZ * sa;
    float lz = outX * sa + outZ * ca;

    // Width direction (perpendicular to lean in XZ plane)
    float wx = -lz, wz = lx;

    // 4 cross-sections: leaf shape (wide base, widest at 1/3, taper, round tip)
    float leanMid1 = lean * midLeanRatio * 0.5f;
    float leanMid2 = lean * (midLeanRatio + (1.0f - midLeanRatio) * 0.5f);
    struct Section { float x, y, z, hw; };
    Section secs[4] = {
        { px,                                1.0f,                  pz,                                baseW * 0.50f },  // base
        { px + leanMid1 * lx,                1.0f + height * 0.30f, pz + leanMid1 * lz,                baseW * 0.55f },  // widest (leaf belly)
        { px + leanMid2 * lx,                1.0f + height * 0.70f, pz + leanMid2 * lz,                baseW * 0.30f },  // taper
        { px + lean * lx,                    1.0f + height,         pz + lean * lz,                    baseW * 0.10f },  // rounded tip
    };

    for (int s = 0; s < 3; s++) {
        const auto& s0 = secs[s];
        const auto& s1 = secs[s + 1];

        float l0x = s0.x - s0.hw * wx, l0z = s0.z - s0.hw * wz;
        float r0x = s0.x + s0.hw * wx, r0z = s0.z + s0.hw * wz;
        float l1x = s1.x - s1.hw * wx, l1z = s1.z - s1.hw * wz;
        float r1x = s1.x + s1.hw * wx, r1z = s1.z + s1.hw * wz;

        float tx = s1.x - s0.x, ty = s1.y - s0.y, tz = s1.z - s0.z;
        float nx = ty * wz;
        float ny = tz * wx - tx * wz;
        float nz = -ty * wx;
        float nlen = sqrtf(nx * nx + ny * ny + nz * nz);
        if (nlen > 1e-6f) { nx /= nlen; ny /= nlen; nz /= nlen; }
        else { nx = lx; ny = 0; nz = lz; }

        // Front face
        emitTri(verts, l0x, s0.y, l0z, r0x, s0.y, r0z, l1x, s1.y, l1z, nx, ny, nz);
        emitTri(verts, r0x, s0.y, r0z, r1x, s1.y, r1z, l1x, s1.y, l1z, nx, ny, nz);

        // Back face (flipped normal — emitTri auto-corrects winding)
        emitTri(verts, l0x, s0.y, l0z, r0x, s0.y, r0z, l1x, s1.y, l1z, -nx, -ny, -nz);
        emitTri(verts, r0x, s0.y, r0z, r1x, s1.y, r1z, l1x, s1.y, l1z, -nx, -ny, -nz);
    }
}

// ═════════════════════════════════════════════════════════════════
// TopingSystem implementation
// ═════════════════════════════════════════════════════════════════

void TopingSystem::initialize() {
    registerDefs();
    generateMeshes();
}

// ── Register toping types ───────────────────────────────────────
void TopingSystem::registerDefs() {
    defs_.clear();

    // Type 0: Stone bevel — clean angular ridge along open edges
    {
        TopingDef def{};
        def.materialID = 3;
        def.face       = FACE_POS_Y;
        def.priority   = 0;
        def.height     = 0.06f;
        def.width      = 0.12f;
        def.segments   = 1;
        defs_.push_back(def);
    }

    // Type 1: Grass blades — individual grass blades along open edges
    {
        TopingDef def{};
        def.materialID = 1;
        def.face       = FACE_POS_Y;
        def.priority   = 1;
        def.height     = 0.20f;  // reference height (blade heights vary per preset)
        def.width      = 0.10f;  // reference inset
        def.segments   = 2;      // blade segments (curvature)
        defs_.push_back(def);
    }
}

// ── Generate all 16 mesh variants per def ───────────────────────
void TopingSystem::generateMeshes() {
    vertices_.clear();
    for (auto& def : defs_) {
        for (int bitmask = 0; bitmask < 16; bitmask++) {
            generateVariant(def, (uint8_t)bitmask);
        }
    }
}

// ── Dispatch to material-specific generation ─────────────────────
void TopingSystem::generateVariant(TopingDef& def, uint8_t bitmask) {
    const uint32_t startOffset = (uint32_t)vertices_.size();

    if (def.materialID == 3)
        generateStoneVariant(def, bitmask);
    else
        generateGrassVariant(def, bitmask);

    const uint32_t count = (uint32_t)vertices_.size() - startOffset;
    def.variants[bitmask] = { startOffset, count };
}

// ═════════════════════════════════════════════════════════════════
// Stone bevel generation
// ═════════════════════════════════════════════════════════════════
// Features:
//   1. Bevel strips along each open edge (outer wall + slope)
//   2. Corner fill triangles where two adjacent open edges meet
//   3. Cap triangles where a strip end meets a connected edge
//
void TopingSystem::generateStoneVariant(TopingDef& def, uint8_t bitmask) {
    const float h = def.height;
    const float w = def.width;

    // ── 1. Bevel strips for open edges ──────────────────────────
    for (int edge = 0; edge < 4; edge++) {
        if (bitmask & (1 << edge)) continue;

        const EdgeDef& e = kEdges[edge];
        const float dx = e.ex - e.sx;
        const float dz = e.ez - e.sz;

        // Outer wall (vertical, facing outward)
        emitTri(vertices_,
            e.sx, 1.0f + h, e.sz,   e.ex, 1.0f + h, e.ez,   e.sx, 1.0f, e.sz,
            e.nx, 0.0f, e.nz);
        emitTri(vertices_,
            e.ex, 1.0f + h, e.ez,   e.ex, 1.0f, e.ez,   e.sx, 1.0f, e.sz,
            e.nx, 0.0f, e.nz);

        // Slope (facing inward + up)
        float slopeLen = sqrtf(h * h + w * w);
        if (slopeLen < 1e-4f) slopeLen = 1.0f;
        float cnx = e.ix * (h / slopeLen);
        float cny = w / slopeLen;
        float cnz = e.iz * (h / slopeLen);

        float inSx = e.sx + w * e.ix, inSz = e.sz + w * e.iz;
        float inEx = e.ex + w * e.ix, inEz = e.ez + w * e.iz;

        emitTri(vertices_,
            e.sx, 1.0f + h, e.sz,   inSx, 1.0f, inSz,   e.ex, 1.0f + h, e.ez,
            cnx, cny, cnz);
        emitTri(vertices_,
            e.ex, 1.0f + h, e.ez,   inSx, 1.0f, inSz,   inEx, 1.0f, inEz,
            cnx, cny, cnz);

        // ── 3. Caps at strip endpoints ──────────────────────────
        // At start: if the other edge sharing this corner is CONNECTED
        int startNeighbor = kStartNeighbor[edge];
        if (bitmask & (1 << startNeighbor)) {
            // Cap triangle at strip start, facing -stripDir
            float capNx = -dx, capNz = -dz;
            float innerX = e.sx + w * e.ix;
            float innerZ = e.sz + w * e.iz;
            emitTri(vertices_,
                e.sx, 1.0f, e.sz,   e.sx, 1.0f + h, e.sz,   innerX, 1.0f, innerZ,
                capNx, 0.0f, capNz);
        }

        // At end: if the other edge sharing this corner is CONNECTED
        int endNeighbor = kEndNeighbor[edge];
        if (bitmask & (1 << endNeighbor)) {
            // Cap triangle at strip end, facing +stripDir
            float capNx = dx, capNz = dz;
            float innerX = e.ex + w * e.ix;
            float innerZ = e.ez + w * e.iz;
            emitTri(vertices_,
                e.ex, 1.0f, e.ez,   e.ex, 1.0f + h, e.ez,   innerX, 1.0f, innerZ,
                capNx, 0.0f, capNz);
        }
    }

    // ── 2. Corner fills where two adjacent open edges meet ──────
    for (int c = 0; c < 4; c++) {
        const auto& corner = kCorners[c];
        // Both edges open → fill the slope gap at the corner
        if ((bitmask & (1 << corner.bitA)) == 0 &&
            (bitmask & (1 << corner.bitB)) == 0)
        {
            const EdgeDef& eA = kEdges[corner.bitA];
            const EdgeDef& eB = kEdges[corner.bitB];

            // Peak at corner (shared by both edges)
            float peakX = corner.cx, peakZ = corner.cz;

            // Inner points from each edge's inward direction
            float innerAx = corner.cx + w * eA.ix;
            float innerAz = corner.cz + w * eA.iz;
            float innerBx = corner.cx + w * eB.ix;
            float innerBz = corner.cz + w * eB.iz;

            // Slope normal: compute from cross product, ensure upward
            float e1x = innerAx - peakX, e1y = -h, e1z = innerAz - peakZ;
            float e2x = innerBx - peakX, e2y = -h, e2z = innerBz - peakZ;
            float fnx = e1y * e2z - e1z * e2y;
            float fny = e1z * e2x - e1x * e2z;
            float fnz = e1x * e2y - e1y * e2x;
            if (fny < 0) { fnx = -fnx; fny = -fny; fnz = -fnz; }
            float fnlen = sqrtf(fnx * fnx + fny * fny + fnz * fnz);
            if (fnlen > 1e-6f) { fnx /= fnlen; fny /= fnlen; fnz /= fnlen; }
            else { fnx = 0; fny = 1; fnz = 0; }

            emitTri(vertices_,
                peakX, 1.0f + h, peakZ,
                innerAx, 1.0f, innerAz,
                innerBx, 1.0f, innerBz,
                fnx, fny, fnz);
        }
    }
}

// ═════════════════════════════════════════════════════════════════
// Grass blade generation — tuft-based
// ═════════════════════════════════════════════════════════════════
// Grass is generated as clusters ("tufts") of blades that fan out
// from a shared base point. Each blade within a tuft has unique
// height, width, orientation, curvature, and lean — all derived
// deterministically from hash functions for reproducibility.
//
// Tuft placement along open edges; extra tufts at open corners.
// Density: fewer open edges → denser tufts per edge (hedge effect).
//
void TopingSystem::generateGrassVariant(TopingDef& def, uint8_t bitmask) {
    // Count open edges
    int openEdges = 0;
    for (int b = 0; b < 4; b++)
        if (!(bitmask & (1 << b))) openEdges++;

    if (openEdges == 0) return;

    // Tuft count per edge (more open edges → fewer tufts each)
    int tuftsPerEdge;
    switch (openEdges) {
        case 1:  tuftsPerEdge = 9; break;
        case 2:  tuftsPerEdge = 7; break;
        case 3:  tuftsPerEdge = 5; break;
        default: tuftsPerEdge = 5; break;
    }

    // ── 1. Tufts along open edges ───────────────────────────────
    for (int edge = 0; edge < 4; edge++) {
        if (bitmask & (1 << edge)) continue;

        const EdgeDef& e = kEdges[edge];
        const float dx = e.ex - e.sx;
        const float dz = e.ez - e.sz;

        for (int ti = 0; ti < tuftsPerEdge; ti++) {
            // ── Tuft CENTER position ─────────────────────────────
            // Along edge: fully random
            float t = hashF(edge * 7 + 1, ti * 13 + 3, 99);
            t = 0.03f + t * 0.94f;

            // Distance from edge: 0.0 (flush) to 0.45 (near center)
            // Use squared hash to bias toward edge while allowing far tufts
            float edgeDistHash = hashF(edge, ti, 44);
            float tuftInset = edgeDistHash * edgeDistHash * 0.30f; // quadratic bias

            float tuftCenterX = e.sx + t * dx + tuftInset * e.ix;
            float tuftCenterZ = e.sz + t * dz + tuftInset * e.iz;

            // ── Per-tuft personality ─────────────────────────────
            float tuftHeightScale = 0.40f + hashF(edge, ti, 77) * 0.60f;
            float tuftLeanScale = 0.2f + hashF(edge, ti, 55) * 0.8f;
            int bladesInTuft = 5 + (int)(hashF(edge, ti, 33) * 6.99f);

            // Emit blades tightly clustered around tuft center
            for (int bi = 0; bi < bladesInTuft; bi++) {
                float h1 = hashF(edge, ti, bi * 3 + 0);
                float h2 = hashF(edge, ti, bi * 3 + 1);
                float h3 = hashF(edge, ti, bi * 3 + 2);
                float h4 = hashF(edge + 4, ti, bi * 3 + 0);
                float h5 = hashF(edge + 4, ti, bi * 3 + 1);

                // Fan angle
                float spreadAngle = 55.0f;
                float fanT = (bladesInTuft > 1)
                    ? (float)bi / (float)(bladesInTuft - 1)
                    : 0.5f;
                float angle = (fanT - 0.5f) * 2.0f * spreadAngle;
                angle += (h1 - 0.5f) * 20.0f;

                // Height: short leaf-like blades
                float bladeHeight = (0.08f + h2 * 0.20f) * tuftHeightScale;

                float baseWidth = 0.055f + h3 * 0.040f;  // wider blades
                float lean = (0.02f + h4 * 0.08f) * tuftLeanScale;
                float midLean = 0.08f + h5 * 0.35f;

                // Spread blades within tuft (±0.07 — clearly separated)
                float h6 = hashF(edge + 8, ti, bi * 3 + 2);
                float offX = (h1 - 0.5f) * 0.14f;
                float offZ = (h6 - 0.5f) * 0.14f;

                float bx = tuftCenterX + offX;
                float bz = tuftCenterZ + offZ;

                emitBlade(vertices_, bx, bz, e.nx, e.nz,
                          bladeHeight, baseWidth, lean, angle, midLean);
            }
        }
    }

    // ── 2. Corner tufts where two adjacent open edges meet ──────
    for (int c = 0; c < 4; c++) {
        const auto& corner = kCorners[c];
        if ((bitmask & (1 << corner.bitA)) != 0 ||
            (bitmask & (1 << corner.bitB)) != 0) continue;

        const EdgeDef& eA = kEdges[corner.bitA];
        const EdgeDef& eB = kEdges[corner.bitB];

        // Diagonal outward direction (bisector of two edge normals)
        float diagX = eA.nx + eB.nx;
        float diagZ = eA.nz + eB.nz;
        float diagLen = sqrtf(diagX * diagX + diagZ * diagZ);
        if (diagLen > 1e-6f) { diagX /= diagLen; diagZ /= diagLen; }

        // Corner tuft: cluster filling the diagonal gap
        int cornerBlades = 5 + (int)(hashF(c + 10, 0, 33) * 6.99f);
        // Corner tuft inset: 0.05 to 0.35 diagonally inward
        float cDistHash = hashF(c + 10, 0, 44);
        float cornerInset = 0.05f + cDistHash * 0.30f;
        float cbx = corner.cx + cornerInset * (eA.ix + eB.ix);
        float cbz = corner.cz + cornerInset * (eA.iz + eB.iz);

        float cornerHeightScale = 0.40f + hashF(c + 10, 0, 77) * 0.60f;
        float cornerLeanScale   = 0.3f + hashF(c + 10, 0, 55) * 1.5f;

        for (int bi = 0; bi < cornerBlades; bi++) {
            float h1 = hashF(c + 10, bi, 0);
            float h2 = hashF(c + 10, bi, 1);
            float h3 = hashF(c + 10, bi, 2);
            float h4 = hashF(c + 10, bi, 3);
            float h5 = hashF(c + 10, bi, 4);

            // Wide fan across the diagonal
            float fanT = (float)bi / (float)(cornerBlades - 1);
            float angle = (fanT - 0.5f) * 2.0f * 70.0f;
            angle += (h1 - 0.5f) * 15.0f;

            float height    = (0.08f + h2 * 0.20f) * cornerHeightScale;
            float baseWidth = 0.055f + h3 * 0.040f;
            float lean      = (0.02f + h4 * 0.06f) * cornerLeanScale;
            float midLean   = 0.10f + h5 * 0.30f;

            // Tight scatter around corner tuft center (clustered)
            float h6 = hashF(c + 10, bi, 5);
            float offX = (h1 - 0.5f) * 0.06f;
            float offZ = (h6 - 0.5f) * 0.06f;

            emitBlade(vertices_, cbx + offX, cbz + offZ, diagX, diagZ,
                      height, baseWidth, lean, angle, midLean);
        }
    }
}

// ── Collect toping instances from the world ─────────────────────
// Scans every exposed voxel face that matches a registered TopingDef,
// computes the 4-bit adjacency bitmask, and emits a TopingInstance.
//
// Currently only supports FACE_POS_Y (top face). For other faces,
// the adjacency directions would need to be adapted to the face plane.
void TopingSystem::collectInstances(const VoxelWorld& world) {
    instances_.clear();

    // Quick lookup: material -> toping def index (-1 if none)
    int8_t matToDef[256];
    memset(matToDef, -1, sizeof(matToDef));
    for (size_t i = 0; i < defs_.size(); i++) {
        matToDef[defs_[i].materialID] = (int8_t)i;
    }

    world.forEachChunk([&](const ChunkPos& cpos, const Chunk& chunk) {
        for (int z = 0; z < CHUNK_SIZE; z++) {
            for (int y = 0; y < CHUNK_SIZE; y++) {
                for (int x = 0; x < CHUNK_SIZE; x++) {
                    const VoxelData& v = chunk.at(x, y, z);
                    if (v.isEmpty()) continue;

                    const uint8_t mat = v.getMaterialID();
                    const int8_t defIdx = matToDef[mat];
                    if (defIdx < 0) continue;

                    const TopingDef& def = defs_[defIdx];

                    // World coordinates
                    const int wx = cpos.x * CHUNK_SIZE + x;
                    const int wy = cpos.y * CHUNK_SIZE + y;
                    const int wz = cpos.z * CHUNK_SIZE + z;

                    // Check if the target face is exposed
                    if (def.face == FACE_POS_Y) {
                        if (!world.getVoxel(wx, wy + 1, wz).isEmpty()) continue;

                        // Compute 4-bit adjacency bitmask
                        uint8_t adj = 0;
                        const uint8_t myPriority = def.priority;

                        auto checkNeighbor = [&](int nx, int nz) -> bool {
                            uint8_t nMat = world.getVoxel(nx, wy, nz).getMaterialID();
                            if (nMat == 0) return false;
                            if (!world.getVoxel(nx, wy + 1, nz).isEmpty()) return false;
                            if (nMat == mat) return true;
                            int8_t nDefIdx = matToDef[nMat];
                            if (nDefIdx >= 0 && defs_[nDefIdx].priority >= myPriority) return true;
                            return false;
                        };

                        if (checkNeighbor(wx + 1, wz)) adj |= 1; // +X
                        if (checkNeighbor(wx - 1, wz)) adj |= 2; // -X
                        if (checkNeighbor(wx, wz + 1)) adj |= 4; // +Z
                        if (checkNeighbor(wx, wz - 1)) adj |= 8; // -Z

                        instances_.push_back({
                            (float)wx, (float)wy, (float)wz,
                            (uint16_t)defIdx, adj
                        });
                    }
                }
            }
        }
    });
}

void TopingSystem::collectInstancesParallel(const VoxelWorld& world) {
    instances_.clear();

    // Quick lookup: material -> toping def index (-1 if none)
    int8_t matToDef[256];
    memset(matToDef, -1, sizeof(matToDef));
    for (size_t i = 0; i < defs_.size(); i++) {
        matToDef[defs_[i].materialID] = (int8_t)i;
    }

    // Collect chunk pointers for parallel dispatch
    std::vector<const Chunk*> chunkPtrs;
    std::vector<ChunkPos> chunkPositions;
    world.forEachChunk([&](const ChunkPos& cpos, const Chunk& chunk) {
        chunkPtrs.push_back(&chunk);
        chunkPositions.push_back(cpos);
    });

    const uint32_t numChunks = (uint32_t)chunkPtrs.size();
    if (numChunks == 0) return;

    // Per-chunk local instance vectors (no locking needed)
    std::vector<std::vector<TopingInstance>> perChunkInstances(numChunks);

    wi::jobsystem::context ctx;
    wi::jobsystem::Dispatch(ctx, numChunks, 1,
        [&](wi::jobsystem::JobArgs args) {
            const uint32_t ci = args.jobIndex;
            const Chunk& chunk = *chunkPtrs[ci];
            const ChunkPos& cpos = chunkPositions[ci];
            auto& localInstances = perChunkInstances[ci];

            // Pre-cache neighbor chunks: self + ±X, ±Z, +Y (5 directions used)
            // Layout: [dx+1][dz+1] for same-Y neighbors, plus +Y neighbors
            const Chunk* ncXZ[3][3] = {};   // neighbors at same Y
            const Chunk* ncXZup[3][3] = {}; // neighbors at Y+1
            for (int dz = -1; dz <= 1; dz++)
            for (int dx = -1; dx <= 1; dx++) {
                ncXZ[dx+1][dz+1] = world.getChunk(
                    ChunkPos{cpos.x + dx, cpos.y, cpos.z + dz});
                ncXZup[dx+1][dz+1] = world.getChunk(
                    ChunkPos{cpos.x + dx, cpos.y + 1, cpos.z + dz});
            }

            // Fast voxel read using cached chunk pointers
            auto readVoxel = [&](int wx, int wy, int wz) -> VoxelData {
                int lx = wx - cpos.x * CHUNK_SIZE;
                int ly = wy - cpos.y * CHUNK_SIZE;
                int lz = wz - cpos.z * CHUNK_SIZE;
                int cx = (lx < 0) ? 0 : (lx >= CHUNK_SIZE) ? 2 : 1;
                int cz = (lz < 0) ? 0 : (lz >= CHUNK_SIZE) ? 2 : 1;
                const Chunk* nc;
                if (ly >= 0 && ly < CHUNK_SIZE) {
                    nc = ncXZ[cx][cz];
                } else if (ly >= CHUNK_SIZE && ly < CHUNK_SIZE * 2) {
                    nc = ncXZup[cx][cz];
                    ly -= CHUNK_SIZE;
                } else {
                    return VoxelData{}; // out of cached range
                }
                if (!nc) return VoxelData{};
                int flx = ((lx % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
                int flz = ((lz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
                return nc->at(flx, ly, flz);
            };

            for (int z = 0; z < CHUNK_SIZE; z++) {
                for (int y = 0; y < CHUNK_SIZE; y++) {
                    for (int x = 0; x < CHUNK_SIZE; x++) {
                        const VoxelData& v = chunk.at(x, y, z);
                        if (v.isEmpty()) continue;

                        const uint8_t mat = v.getMaterialID();
                        const int8_t defIdx = matToDef[mat];
                        if (defIdx < 0) continue;

                        const TopingDef& def = defs_[defIdx];

                        const int wx = cpos.x * CHUNK_SIZE + x;
                        const int wy = cpos.y * CHUNK_SIZE + y;
                        const int wz = cpos.z * CHUNK_SIZE + z;

                        if (def.face == FACE_POS_Y) {
                            if (!readVoxel(wx, wy + 1, wz).isEmpty()) continue;

                            uint8_t adj = 0;
                            const uint8_t myPriority = def.priority;

                            auto checkNeighbor = [&](int nx, int nz) -> bool {
                                uint8_t nMat = readVoxel(nx, wy, nz).getMaterialID();
                                if (nMat == 0) return false;
                                if (!readVoxel(nx, wy + 1, nz).isEmpty()) return false;
                                if (nMat == mat) return true;
                                int8_t nDefIdx = matToDef[nMat];
                                if (nDefIdx >= 0 && defs_[nDefIdx].priority >= myPriority) return true;
                                return false;
                            };

                            if (checkNeighbor(wx + 1, wz)) adj |= 1;
                            if (checkNeighbor(wx - 1, wz)) adj |= 2;
                            if (checkNeighbor(wx, wz + 1)) adj |= 4;
                            if (checkNeighbor(wx, wz - 1)) adj |= 8;

                            localInstances.push_back({
                                (float)wx, (float)wy, (float)wz,
                                (uint16_t)defIdx, adj
                            });
                        }
                    }
                }
            }
        });
    wi::jobsystem::Wait(ctx);

    // Merge per-chunk instances
    size_t total = 0;
    for (auto& v : perChunkInstances) total += v.size();
    instances_.reserve(total);
    for (auto& v : perChunkInstances) {
        instances_.insert(instances_.end(), v.begin(), v.end());
    }
}

} // namespace voxel
