#include "VoxelMesher.h"
#include <cstring>
#include <algorithm>

namespace voxel {

// ── Build binary masks per axis ─────────────────────────────────
// For each axis, solid[u][v] is a 32-bit mask where bit i=1 means
// voxel at position i along that axis is solid.
void VoxelMesher::buildAxisMasks(const Chunk& chunk, AxisMasks masks[3]) {
    std::memset(masks, 0, sizeof(AxisMasks) * 3);

    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);
                // Phase 5: smooth voxels are still "solid" for face culling
                // (they block neighbor faces) but don't emit their own quads.
                // So they must be in the solid mask.
                if (!v.isEmpty()) {
                    // X-axis: march along X, indexed by [Y][Z]
                    masks[0].solid[y][z] |= (1u << x);
                    // Y-axis: march along Y, indexed by [X][Z]
                    masks[1].solid[x][z] |= (1u << y);
                    // Z-axis: march along Z, indexed by [X][Y]
                    masks[2].solid[x][y] |= (1u << z);
                }
            }
        }
    }
}

// Helper: get voxel, considering neighbor chunks for boundary faces
static VoxelData getVoxelSafe(const Chunk& chunk, const VoxelWorld& world, int x, int y, int z) {
    if (chunk.isInBounds(x, y, z)) {
        return chunk.at(x, y, z);
    }
    // Cross-chunk lookup
    int wx = chunk.pos.x * CHUNK_SIZE + x;
    int wy = chunk.pos.y * CHUNK_SIZE + y;
    int wz = chunk.pos.z * CHUNK_SIZE + z;
    return world.getVoxel(wx, wy, wz);
}

// ── Greedy Merge ────────────────────────────────────────────────
// For a given face direction, merge visible faces of the same material
// into maximal rectangular quads.
void VoxelMesher::greedyMerge(
    const Chunk& chunk,
    const VoxelWorld& world,
    uint8_t face,
    const uint32_t faceMasks[CHUNK_SIZE][CHUNK_SIZE],
    std::vector<PackedQuad>& outQuads
) {
    // Determine axis mapping based on face
    // face 0,1 = X axis -> iterate over Y,Z slices
    // face 2,3 = Y axis -> iterate over X,Z slices
    // face 4,5 = Z axis -> iterate over X,Y slices

    // For each slice along the face normal axis
    for (int depth = 0; depth < CHUNK_SIZE; depth++) {
        // Build a 2D grid of material IDs for this slice
        uint8_t matGrid[CHUNK_SIZE][CHUNK_SIZE];
        bool visited[CHUNK_SIZE][CHUNK_SIZE];
        std::memset(matGrid, 0, sizeof(matGrid));
        std::memset(visited, 0, sizeof(visited));

        int faceCount = 0;
        for (int v = 0; v < CHUNK_SIZE; v++) {
            for (int u = 0; u < CHUNK_SIZE; u++) {
                // Check if this face is visible at this depth
                bool faceVisible = false;

                int x, y, z;
                switch (face) {
                case FACE_POS_X: x = depth; y = u; z = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
                case FACE_NEG_X: x = depth; y = u; z = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
                case FACE_POS_Y: y = depth; x = u; z = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
                case FACE_NEG_Y: y = depth; x = u; z = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
                case FACE_POS_Z: z = depth; x = u; y = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
                case FACE_NEG_Z: z = depth; x = u; y = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
                }

                if (faceVisible) {
                    // Phase 5: skip smooth voxels (they have Surface Nets mesh)
                    const VoxelData& vd = chunk.at(x, y, z);
                    if (vd.isSmooth()) continue;
                    matGrid[v][u] = vd.getMaterialID();
                    faceCount++;
                }
            }
        }

        if (faceCount == 0) continue;

        // Greedy merge: scan row by row, merge same-material quads
        for (int v = 0; v < CHUNK_SIZE; v++) {
            for (int u = 0; u < CHUNK_SIZE; u++) {
                if (visited[v][u] || matGrid[v][u] == 0) continue;

                uint8_t mat = matGrid[v][u];

                // Expand width (along u)
                int w = 1;
                while (u + w < CHUNK_SIZE && !visited[v][u + w] && matGrid[v][u + w] == mat) {
                    w++;
                }

                // Expand height (along v)
                int h = 1;
                bool canExpand = true;
                while (v + h < CHUNK_SIZE && canExpand) {
                    for (int du = 0; du < w; du++) {
                        if (visited[v + h][u + du] || matGrid[v + h][u + du] != mat) {
                            canExpand = false;
                            break;
                        }
                    }
                    if (canExpand) h++;
                }

                // Mark as visited
                for (int dv = 0; dv < h; dv++) {
                    for (int du = 0; du < w; du++) {
                        visited[v + dv][u + du] = true;
                    }
                }

                // Compute the actual position in chunk-local coords
                uint8_t px, py, pz;
                switch (face) {
                case FACE_POS_X: px = (uint8_t)depth; py = (uint8_t)u; pz = (uint8_t)v; break;
                case FACE_NEG_X: px = (uint8_t)depth; py = (uint8_t)u; pz = (uint8_t)v; break;
                case FACE_POS_Y: px = (uint8_t)u; py = (uint8_t)depth; pz = (uint8_t)v; break;
                case FACE_NEG_Y: px = (uint8_t)u; py = (uint8_t)depth; pz = (uint8_t)v; break;
                case FACE_POS_Z: px = (uint8_t)u; py = (uint8_t)v; pz = (uint8_t)depth; break;
                case FACE_NEG_Z: px = (uint8_t)u; py = (uint8_t)v; pz = (uint8_t)depth; break;
                default: px = py = pz = 0; break;
                }

                // Width/height in the quad's local UV space
                uint8_t qw, qh;
                switch (face) {
                case FACE_POS_X: case FACE_NEG_X: qw = (uint8_t)w; qh = (uint8_t)h; break;
                case FACE_POS_Y: case FACE_NEG_Y: qw = (uint8_t)w; qh = (uint8_t)h; break;
                case FACE_POS_Z: case FACE_NEG_Z: qw = (uint8_t)w; qh = (uint8_t)h; break;
                default: qw = qh = 1; break;
                }

                outQuads.push_back(PackedQuad::create(
                    px, py, pz, qw, qh, face, mat, 0, 0
                ));
            }
        }
    }
}

uint32_t VoxelMesher::meshChunk(Chunk& chunk, const VoxelWorld& world) {
    chunk.quads.clear();

    // Step 1: Build binary solid masks per axis
    AxisMasks axisMasks[3];
    buildAxisMasks(chunk, axisMasks);

    // Step 2: For each face direction, compute visible face masks
    // then do greedy merge
    for (uint8_t face = 0; face < FACE_COUNT; face++) {
        int axis = face / 2;       // 0=X, 1=Y, 2=Z
        bool positive = (face % 2 == 0);

        // Compute visible face masks
        // A face is visible if the voxel is solid and the neighbor in the face direction is air
        uint32_t faceMasks[CHUNK_SIZE][CHUNK_SIZE];
        std::memset(faceMasks, 0, sizeof(faceMasks));

        for (int v = 0; v < CHUNK_SIZE; v++) {
            for (int u = 0; u < CHUNK_SIZE; u++) {
                uint32_t solid = axisMasks[axis].solid[u][v];
                if (solid == 0) continue;

                uint32_t visible;
                if (positive) {
                    // +dir: face visible if solid here and NOT solid at pos+1
                    // Shift right: neighbor is at bit+1
                    uint32_t neighbor = (solid >> 1);
                    // The highest bit has no neighbor in this chunk - check boundary
                    visible = solid & ~neighbor;
                    // Bit 31 (chunk boundary): need to check neighbor chunk
                    // For now, always show boundary faces
                    if (solid & (1u << (CHUNK_SIZE - 1))) {
                        // Check if neighbor chunk's voxel is empty
                        int nx, ny, nz;
                        switch (axis) {
                        case 0: nx = CHUNK_SIZE; ny = u; nz = v; break; // X+
                        case 1: nx = u; ny = CHUNK_SIZE; nz = v; break; // Y+
                        case 2: nx = u; ny = v; nz = CHUNK_SIZE; break; // Z+
                        default: nx = ny = nz = 0;
                        }
                        if (!getVoxelSafe(chunk, world, nx, ny, nz).isEmpty()) {
                            visible &= ~(1u << (CHUNK_SIZE - 1)); // hide boundary face
                        }
                    }
                } else {
                    // -dir: face visible if solid here and NOT solid at pos-1
                    uint32_t neighbor = (solid << 1);
                    visible = solid & ~neighbor;
                    // Bit 0 (chunk boundary)
                    if (solid & 1u) {
                        int nx, ny, nz;
                        switch (axis) {
                        case 0: nx = -1; ny = u; nz = v; break;
                        case 1: nx = u; ny = -1; nz = v; break;
                        case 2: nx = u; ny = v; nz = -1; break;
                        default: nx = ny = nz = 0;
                        }
                        if (!getVoxelSafe(chunk, world, nx, ny, nz).isEmpty()) {
                            visible &= ~1u; // hide boundary face
                        }
                    }
                }

                faceMasks[u][v] = visible;
            }
        }

        uint32_t beforeCount = (uint32_t)chunk.quads.size();
        greedyMerge(chunk, world, face, faceMasks, chunk.quads);
        chunk.faceOffsets[face] = beforeCount;
        chunk.faceCounts[face] = (uint32_t)chunk.quads.size() - beforeCount;
    }

    chunk.quadCount = (uint32_t)chunk.quads.size();
    chunk.dirty = false;
    return chunk.quadCount;
}

uint8_t VoxelMesher::calcAO(const VoxelWorld& world, const ChunkPos& cpos,
                             int x, int y, int z, uint8_t face) {
    // Simplified AO: count occluding neighbors around the vertex
    // Returns packed 4x2-bit AO for the 4 corners
    // TODO: implement proper per-corner AO
    return 0;
}

// ══════════════════════════════════════════════════════════════════
// ── Naive Surface Nets Mesher (Phase 5) ─────────────────────────
// ══════════════════════════════════════════════════════════════════
//
// Algorithm:
//   1. Compute SDF for each voxel: smooth solid = -1, empty = +1
//      Non-smooth solid voxels act as hard walls (SDF crushed to -1).
//   2. For each cell on the surface (SDF sign differs from at least one neighbor),
//      place a vertex at the centroid of edge crossings.
//   3. For each edge (pair of adjacent cells) with a sign change,
//      emit a quad connecting the 4 cells that share that edge, then split to 2 triangles.
//   4. Normals derived from SDF gradient (central differences).

// Padded grid: +2 border for cross-chunk SDF lookups and neighbor smooth detection
static constexpr int PAD = 2;
static constexpr int GRID = CHUNK_SIZE + 2 * PAD; // 36

static inline int gridIdx(int x, int y, int z) {
    return (x + PAD) + (y + PAD) * GRID + (z + PAD) * GRID * GRID;
}

// Helper: read voxel data at chunk-local coords (with cross-chunk fallback)
static VoxelData readVoxel(const Chunk& chunk, const VoxelWorld& world, int x, int y, int z) {
    if (chunk.isInBounds(x, y, z))
        return chunk.at(x, y, z);
    return world.getVoxel(
        chunk.pos.x * CHUNK_SIZE + x,
        chunk.pos.y * CHUNK_SIZE + y,
        chunk.pos.z * CHUNK_SIZE + z);
}

float SmoothMesher::computeSDF(const Chunk& chunk, const VoxelWorld& world,
                                int x, int y, int z) {
    VoxelData v = readVoxel(chunk, world, x, y, z);
    if (v.isEmpty()) return 1.0f;       // empty → positive SDF
    return -1.0f;                        // any solid → negative SDF
}

void SmoothMesher::computeNormal(const Chunk& chunk, const VoxelWorld& world,
                                  int x, int y, int z,
                                  float& nx, float& ny, float& nz) {
    // Central differences of the SDF
    float dx = computeSDF(chunk, world, x+1, y, z) - computeSDF(chunk, world, x-1, y, z);
    float dy = computeSDF(chunk, world, x, y+1, z) - computeSDF(chunk, world, x, y-1, z);
    float dz = computeSDF(chunk, world, x, y, z+1) - computeSDF(chunk, world, x, y, z-1);

    float len = std::sqrt(dx*dx + dy*dy + dz*dz);
    if (len > 0.0001f) {
        nx = dx / len;
        ny = dy / len;
        nz = dz / len;
    } else {
        nx = 0.0f; ny = 1.0f; nz = 0.0f;
    }
}

// Thread-local scratch buffers to avoid per-chunk allocation overhead.
// Each worker thread gets its own set, eliminating malloc/free thrashing.
struct SmoothScratch {
    float sdf[GRID * GRID * GRID];
    uint8_t smoothGrid[GRID * GRID * GRID];
    uint8_t smoothNear[GRID * GRID * GRID]; // dilated: 1 if smooth OR face-adjacent to smooth
    VoxelData voxelGrid[GRID * GRID * GRID];
    int32_t vertexMap[33 * 33 * 33]; // VERT_RANGE³
};
static thread_local SmoothScratch* tls_scratch = nullptr;

uint32_t SmoothMesher::meshChunk(Chunk& chunk, const VoxelWorld& world) {
    chunk.smoothVertices.clear();
    chunk.hasSmooth = false;

    // ── Early exit: skip chunks far from any smooth voxels ──────
    // Check this chunk + 26 neighbors for containsSmooth flag.
    // This avoids the expensive 36³ grid fill for ~70% of chunks.
    {
        bool nearSmooth = chunk.containsSmooth;
        if (!nearSmooth) {
            for (int dz = -1; dz <= 1 && !nearSmooth; dz++)
            for (int dy = -1; dy <= 1 && !nearSmooth; dy++)
            for (int dx = -1; dx <= 1 && !nearSmooth; dx++) {
                if (dx == 0 && dy == 0 && dz == 0) continue;
                const Chunk* nc = world.getChunk(
                    ChunkPos{chunk.pos.x + dx, chunk.pos.y + dy, chunk.pos.z + dz});
                if (nc && nc->containsSmooth) nearSmooth = true;
            }
        }
        if (!nearSmooth) return 0;
    }

    // Allocate thread-local scratch once per thread (persists across calls)
    if (!tls_scratch) tls_scratch = new SmoothScratch();
    auto& scratch = *tls_scratch;

    // ── Step 1: Build SDF grid + smooth flag grid + voxel cache ──
    // PAD=2 so we have SDF data for cells at [-1..CHUNK_SIZE] (all 8 corners accessible)
    // Also build a "isSmooth" grid for the same range to detect proximity to smooth voxels.
    // voxelGrid caches VoxelData to avoid repeated cross-chunk hashmap lookups later.
    float* sdf = scratch.sdf;
    uint8_t* smoothGrid = scratch.smoothGrid;
    VoxelData* voxelGrid = scratch.voxelGrid;
    constexpr int GRID3 = GRID * GRID * GRID;
    std::memset(smoothGrid, 0, GRID3);
    // SDF defaults to 1.0f (empty) — fill below
    for (int i = 0; i < GRID3; i++) sdf[i] = 1.0f;
    bool anySmooth = false;

    // Pre-cache neighbor chunk pointers for fast cross-chunk access
    const Chunk* neighborChunks[3][3][3] = {};
    for (int dz = -1; dz <= 1; dz++)
    for (int dy = -1; dy <= 1; dy++)
    for (int dx = -1; dx <= 1; dx++) {
        neighborChunks[dx+1][dy+1][dz+1] = world.getChunk(
            ChunkPos{chunk.pos.x + dx, chunk.pos.y + dy, chunk.pos.z + dz});
    }

    // Helper: fast voxel read using cached neighbor chunk pointers
    auto readVoxelFast = [&](int x, int y, int z) -> VoxelData {
        if (x >= 0 && x < CHUNK_SIZE && y >= 0 && y < CHUNK_SIZE && z >= 0 && z < CHUNK_SIZE)
            return chunk.at(x, y, z);
        // Determine which neighbor chunk
        int cx = (x < 0) ? 0 : (x >= CHUNK_SIZE) ? 2 : 1;
        int cy = (y < 0) ? 0 : (y >= CHUNK_SIZE) ? 2 : 1;
        int cz = (z < 0) ? 0 : (z >= CHUNK_SIZE) ? 2 : 1;
        const Chunk* nc = neighborChunks[cx][cy][cz];
        if (!nc) return VoxelData{};  // empty if chunk not loaded
        int lx = ((x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
        int ly = ((y % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
        int lz = ((z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
        return nc->at(lx, ly, lz);
    };

    for (int z = -PAD; z < CHUNK_SIZE + PAD; z++) {
        for (int y = -PAD; y < CHUNK_SIZE + PAD; y++) {
            for (int x = -PAD; x < CHUNK_SIZE + PAD; x++) {
                int gi = gridIdx(x, y, z);
                VoxelData v = readVoxelFast(x, y, z);
                voxelGrid[gi] = v;
                sdf[gi] = v.isEmpty() ? 1.0f : -1.0f;
                if (v.isSmooth()) {
                    smoothGrid[gi] = 1;
                    // Only need anySmooth for this chunk's own voxels
                    if (chunk.isInBounds(x, y, z)) anySmooth = true;
                }
            }
        }
    }

    // Also check 1 beyond the chunk (neighbor chunks may have smooth voxels that
    // affect cells at the chunk boundary)
    if (!anySmooth) {
        // Check if any neighbor voxels just outside the chunk are smooth
        for (int z = -1; z <= CHUNK_SIZE && !anySmooth; z++)
        for (int y = -1; y <= CHUNK_SIZE && !anySmooth; y++)
        for (int x = -1; x <= CHUNK_SIZE && !anySmooth; x++) {
            if (chunk.isInBounds(x, y, z)) continue; // already checked
            if (smoothGrid[gridIdx(x, y, z)]) anySmooth = true;
        }
    }

    if (!anySmooth) return 0;
    chunk.hasSmooth = true;

    // ── Step 1b: Dilate smoothGrid → smoothNear ──────────────────
    // Pre-compute "smooth or face-adjacent to smooth" to reduce the
    // per-cell hasSmooth check from 56 lookups to 8 lookups.
    uint8_t* smoothNear = scratch.smoothNear;
    std::memcpy(smoothNear, smoothGrid, GRID3);
    for (int z = -PAD + 1; z < CHUNK_SIZE + PAD - 1; z++)
    for (int y = -PAD + 1; y < CHUNK_SIZE + PAD - 1; y++)
    for (int x = -PAD + 1; x < CHUNK_SIZE + PAD - 1; x++) {
        if (smoothGrid[gridIdx(x, y, z)]) {
            smoothNear[gridIdx(x+1, y, z)] = 1;
            smoothNear[gridIdx(x-1, y, z)] = 1;
            smoothNear[gridIdx(x, y+1, z)] = 1;
            smoothNear[gridIdx(x, y-1, z)] = 1;
            smoothNear[gridIdx(x, y, z+1)] = 1;
            smoothNear[gridIdx(x, y, z-1)] = 1;
        }
    }

    // ── Step 2: Generate vertices for surface cells ──────────────
    // Extended range: [-1, CHUNK_SIZE) for cross-chunk connectivity.
    // This chunk generates vertices for cells at [-1..CHUNK_SIZE-1].
    // The vertex map covers [-1..CHUNK_SIZE-1] → size = CHUNK_SIZE+1, offset by +1.
    static constexpr int VERT_MIN = -1;
    static constexpr int VERT_MAX = CHUNK_SIZE; // exclusive
    static constexpr int VERT_RANGE = VERT_MAX - VERT_MIN; // CHUNK_SIZE + 1 = 33
    int32_t* vertexMap = scratch.vertexMap;
    std::memset(vertexMap, -1, VERT_RANGE * VERT_RANGE * VERT_RANGE * sizeof(int32_t));

    auto vertMapIdx = [](int x, int y, int z) -> int {
        // shift coordinates by -VERT_MIN = +1 so index range is [0, VERT_RANGE)
        return (x - VERT_MIN) + (y - VERT_MIN) * VERT_RANGE + (z - VERT_MIN) * VERT_RANGE * VERT_RANGE;
    };

    // World offset for this chunk
    float ox = (float)(chunk.pos.x * CHUNK_SIZE);
    float oy = (float)(chunk.pos.y * CHUNK_SIZE);
    float oz = (float)(chunk.pos.z * CHUNK_SIZE);

    // Corner offsets: (dx,dy,dz) for corner index 0-7 of a cell
    static const int cornerOff[8][3] = {
        {0,0,0}, {1,0,0}, {0,1,0}, {1,1,0},
        {0,0,1}, {1,0,1}, {0,1,1}, {1,1,1},
    };
    static const float cornerOffF[8][3] = {
        {0,0,0}, {1,0,0}, {0,1,0}, {1,1,0},
        {0,0,1}, {1,0,1}, {0,1,1}, {1,1,1},
    };
    static const int edges[12][2] = {
        {0,1}, {2,3}, {4,5}, {6,7}, // X-axis edges
        {0,2}, {1,3}, {4,6}, {5,7}, // Y-axis edges
        {0,4}, {1,5}, {2,6}, {3,7}, // Z-axis edges
    };

    for (int z = VERT_MIN; z < VERT_MAX; z++) {
        for (int y = VERT_MIN; y < VERT_MAX; y++) {
            for (int x = VERT_MIN; x < VERT_MAX; x++) {
                // hasSmooth check via dilated grid: at least one corner must be
                // smooth or face-adjacent to smooth. Uses pre-dilated smoothNear
                // grid → only 8 lookups instead of 56.
                bool hasSmooth = false;
                for (int c = 0; c < 8 && !hasSmooth; c++) {
                    if (smoothNear[gridIdx(x + cornerOff[c][0], y + cornerOff[c][1], z + cornerOff[c][2])])
                        hasSmooth = true;
                }
                if (!hasSmooth) continue;

                // Get SDF at 8 corners of cell (x,y,z)
                float corner[8];
                bool hasPos = false, hasNeg = false;
                for (int c = 0; c < 8; c++) {
                    corner[c] = sdf[gridIdx(x + cornerOff[c][0], y + cornerOff[c][1], z + cornerOff[c][2])];
                    if (corner[c] < 0.0f) hasNeg = true;
                    else hasPos = true;
                }

                if (!hasPos || !hasNeg) continue; // no sign change → not on surface

                // Compute vertex position as centroid of edge crossings.
                // +0.5 offset: SDF is sampled at voxel centers, so the cell spans
                // from (x+0.5) to (x+1.5) in world space. This naturally aligns
                // the isosurface with the integer grid (voxel face positions).
                float sumX = 0, sumY = 0, sumZ = 0;
                int crossCount = 0;

                for (int e = 0; e < 12; e++) {
                    float s0 = corner[edges[e][0]];
                    float s1 = corner[edges[e][1]];
                    if ((s0 < 0.0f) == (s1 < 0.0f)) continue;

                    float t = s0 / (s0 - s1);
                    t = std::clamp(t, 0.01f, 0.99f);

                    const float* c0 = cornerOffF[edges[e][0]];
                    const float* c1 = cornerOffF[edges[e][1]];
                    sumX += c0[0] + t * (c1[0] - c0[0]);
                    sumY += c0[1] + t * (c1[1] - c0[1]);
                    sumZ += c0[2] + t * (c1[2] - c0[2]);
                    crossCount++;
                }

                if (crossCount == 0) continue;

                float invCross = 1.0f / (float)crossCount;
                // centroid in [0,1] within the cell
                float cx = sumX * invCross;
                float cy = sumY * invCross;
                float cz = sumZ * invCross;

                // ── Per-axis clamping at blocky boundaries ───────────
                // With +0.5 offset, the cell spans [x+0.5, x+1.5] in world space.
                // The integer grid (blocky faces) is at x+1. In centroid coords,
                // that's centroid = 0.5 (the midpoint of the cell).
                // If the +side corners (dx=1) contain a blocky solid, clamp centroid ≤ 0.5
                // If the -side corners (dx=0) contain a blocky solid, clamp centroid ≥ 0.5
                // This prevents the smooth mesh from extending into blocky territory.
                bool blockyXlo = false, blockyXhi = false;
                bool blockyYlo = false, blockyYhi = false;
                bool blockyZlo = false, blockyZhi = false;
                for (int c = 0; c < 8; c++) {
                    if (corner[c] >= 0.0f) continue; // empty corner
                    VoxelData v = voxelGrid[gridIdx(
                        x + cornerOff[c][0], y + cornerOff[c][1], z + cornerOff[c][2])];
                    if (!v.isEmpty() && !v.isSmooth()) {
                        // This corner is a blocky solid
                        if (cornerOff[c][0] == 0) blockyXlo = true; else blockyXhi = true;
                        if (cornerOff[c][1] == 0) blockyYlo = true; else blockyYhi = true;
                        if (cornerOff[c][2] == 0) blockyZlo = true; else blockyZhi = true;
                    }
                }
                if (blockyXhi) cx = std::min(cx, 0.5f);
                if (blockyXlo) cx = std::max(cx, 0.5f);
                if (blockyYhi) cy = std::min(cy, 0.5f);
                if (blockyYlo) cy = std::max(cy, 0.5f);
                if (blockyZhi) cz = std::min(cz, 0.5f);
                if (blockyZlo) cz = std::max(cz, 0.5f);

                // World position with +0.5 offset (SDF at voxel centers)
                float vx = (float)x + 0.5f + cx;
                float vy = (float)y + 0.5f + cy;
                float vz = (float)z + 0.5f + cz;

                // Determine material: prefer smooth voxels' materials to avoid
                // picking up subsurface blocky materials (e.g., dirt under stone)
                uint8_t smoothMatCounts[256] = {};
                uint8_t allMatCounts[256] = {};
                int smoothCount = 0;
                for (int c = 0; c < 8; c++) {
                    if (corner[c] < 0.0f) {
                        VoxelData v = voxelGrid[gridIdx(
                            x + cornerOff[c][0], y + cornerOff[c][1], z + cornerOff[c][2])];
                        if (!v.isEmpty()) {
                            allMatCounts[v.getMaterialID()]++;
                            if (v.isSmooth()) {
                                smoothMatCounts[v.getMaterialID()]++;
                                smoothCount++;
                            }
                        }
                    }
                }
                // Primary material: prefer smooth-only counts to avoid subsurface bleed
                uint8_t* primaryCounts = (smoothCount > 0) ? smoothMatCounts : allMatCounts;
                uint8_t bestMat = 6, bestCount = 0;
                for (int m = 1; m < 256; m++) {
                    if (primaryCounts[m] > bestCount) {
                        bestMat = (uint8_t)m; bestCount = primaryCounts[m];
                    }
                }
                // Secondary material: only count SURFACE-EXPOSED voxels (at least one
                // empty neighbor). This prevents underground materials (dirt under stone)
                // from bleeding through — same principle as blocky face blending.
                static const int dirs6[6][3] = {{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}};
                uint8_t surfaceMatCounts[256] = {};
                for (int c = 0; c < 8; c++) {
                    if (corner[c] >= 0.0f) continue;
                    int cx = x + cornerOff[c][0], cy = y + cornerOff[c][1], cz = z + cornerOff[c][2];
                    VoxelData v = voxelGrid[gridIdx(cx, cy, cz)];
                    if (v.isEmpty()) continue;
                    // Check if this voxel is on the surface
                    bool onSurface = false;
                    for (int d = 0; d < 6 && !onSurface; d++) {
                        if (sdf[gridIdx(cx + dirs6[d][0], cy + dirs6[d][1], cz + dirs6[d][2])] > 0.0f)
                            onSurface = true;
                    }
                    if (onSurface) surfaceMatCounts[v.getMaterialID()]++;
                }
                uint8_t secMat = bestMat, secCount = 0;
                for (int m = 1; m < 256; m++) {
                    if (m == bestMat) continue;
                    if (surfaceMatCounts[m] > secCount) {
                        secMat = (uint8_t)m; secCount = surfaceMatCounts[m];
                    }
                }
                // blendWeight: binary flag — 255 at material boundary, 0 at interior.
                // GPU interpolation creates the smooth edge-to-interior falloff.
                uint8_t blendW = (secCount > 0 && secMat != bestMat) ? 255 : 0;

                // Store vertex (normals zeroed — computed later from face normals in Step 4)
                int32_t vertIdx = (int32_t)chunk.smoothVertices.size();
                vertexMap[vertMapIdx(x, y, z)] = vertIdx;

                SmoothVertex sv;
                sv.px = ox + vx;
                sv.py = oy + vy;
                sv.pz = oz + vz;
                sv.nx = 0;
                sv.ny = 0;
                sv.nz = 0;
                sv.materialID = bestMat;
                sv.secondaryMat = secMat;
                sv.blendWeight = blendW;
                sv._pad1 = 0;
                sv.chunkIndex = 0;
                sv._pad2 = 0;
                chunk.smoothVertices.push_back(sv);
            }
        }
    }

    if (chunk.smoothVertices.empty()) {
        chunk.hasSmooth = false;
        return 0;
    }

    // ── Step 3: Emit quads for edges with sign change ────────────
    // Canonical ownership: this chunk owns edges whose lower endpoint
    // is in [0, CHUNK_SIZE). Extended to check edges at the chunk
    // boundary (lower endpoint at CHUNK_SIZE-1, upper at CHUNK_SIZE).
    // The sharing cells may be at [-1..CHUNK_SIZE-1], all covered by vertex map.

    // Tri with edge axis info for correct normal orientation.
    // normalAxis: 0=X, 1=Y, 2=Z — the axis of the edge that generated this quad.
    // normalSign: +1 if the normal should point in +axis direction, -1 for -axis.
    struct Tri { int32_t a, b, c; int8_t normalAxis; int8_t normalSign; };
    std::vector<Tri> triangles;
    triangles.reserve(chunk.smoothVertices.size() * 2);

    // Helper: safe vertex map lookup (returns -1 if out of range)
    auto safeVertMap = [&](int x, int y, int z) -> int32_t {
        if (x < VERT_MIN || x >= VERT_MAX ||
            y < VERT_MIN || y >= VERT_MAX ||
            z < VERT_MIN || z >= VERT_MAX) return -1;
        return vertexMap[vertMapIdx(x, y, z)];
    };

    // Helper: emit 2 triangles for a quad (a,b,c,d) with known desired normal.
    // The Y-axis sharing cells have a different spatial arrangement from X and Z,
    // requiring opposite winding to produce correct front-facing triangles.
    auto emitQuad = [&](int a, int b, int c, int d, float s0, int8_t axis) {
        if (a < 0 || b < 0 || c < 0 || d < 0) return;
        int8_t sign = (s0 < 0.0f) ? +1 : -1;
        // Y-axis has natural winding swapped relative to X and Z
        bool useWindingA = (s0 > 0.0f);
        if (axis == 1) useWindingA = !useWindingA;
        if (useWindingA) {
            triangles.push_back({a, b, d, axis, sign});
            triangles.push_back({a, d, c, axis, sign});
        } else {
            triangles.push_back({a, d, b, axis, sign});
            triangles.push_back({a, c, d, axis, sign});
        }
    };

    // Iterate over edges owned by this chunk: grid points [0, CHUNK_SIZE)
    for (int z = 0; z < CHUNK_SIZE; z++) {
        for (int y = 0; y < CHUNK_SIZE; y++) {
            for (int x = 0; x < CHUNK_SIZE; x++) {
                float s0 = sdf[gridIdx(x, y, z)];

                // X-axis edge: (x,y,z) → (x+1,y,z)
                {
                    float s1 = sdf[gridIdx(x+1, y, z)];
                    if ((s0 < 0.0f) != (s1 < 0.0f)) {
                        emitQuad(
                            safeVertMap(x, y-1, z-1), safeVertMap(x, y, z-1),
                            safeVertMap(x, y-1, z),   safeVertMap(x, y, z),
                            s0, 0);
                    }
                }

                // Y-axis edge: (x,y,z) → (x,y+1,z)
                {
                    float s1 = sdf[gridIdx(x, y+1, z)];
                    if ((s0 < 0.0f) != (s1 < 0.0f)) {
                        emitQuad(
                            safeVertMap(x-1, y, z-1), safeVertMap(x, y, z-1),
                            safeVertMap(x-1, y, z),   safeVertMap(x, y, z),
                            s0, 1);
                    }
                }

                // Z-axis edge: (x,y,z) → (x,y,z+1)
                {
                    float s1 = sdf[gridIdx(x, y, z+1)];
                    if ((s0 < 0.0f) != (s1 < 0.0f)) {
                        emitQuad(
                            safeVertMap(x-1, y-1, z), safeVertMap(x, y-1, z),
                            safeVertMap(x-1, y, z),   safeVertMap(x, y, z),
                            s0, 2);
                    }
                }
            }
        }
    }

    // ── Step 4: Compute smooth vertex normals ──────────────────────
    // Accumulate area-weighted face normals into each indexed vertex,
    // then normalize. This gives Gouraud-style smooth shading across
    // the Surface Nets mesh without adding geometry.

    const int vertCount = (int)chunk.smoothVertices.size();

    // Zero out vertex normals (will accumulate face normals)
    for (auto& sv : chunk.smoothVertices) {
        sv.nx = 0; sv.ny = 0; sv.nz = 0;
    }

    // For each triangle: compute oriented face normal, accumulate into vertices.
    // The cross product magnitude is proportional to triangle area, so larger
    // triangles contribute more — this is the standard area-weighted approach.
    for (const auto& tri : triangles) {
        const SmoothVertex& va = chunk.smoothVertices[tri.a];
        const SmoothVertex& vb = chunk.smoothVertices[tri.b];
        const SmoothVertex& vc = chunk.smoothVertices[tri.c];

        float e1x = vb.px - va.px, e1y = vb.py - va.py, e1z = vb.pz - va.pz;
        float e2x = vc.px - va.px, e2y = vc.py - va.py, e2z = vc.pz - va.pz;
        float fnx = e1y * e2z - e1z * e2y;
        float fny = e1z * e2x - e1x * e2z;
        float fnz = e1x * e2y - e1y * e2x;

        // Orient using the known edge axis (same logic as before)
        float component = (tri.normalAxis == 0) ? fnx : (tri.normalAxis == 1) ? fny : fnz;
        if ((component > 0.0f) != (tri.normalSign > 0)) {
            fnx = -fnx; fny = -fny; fnz = -fnz;
        }

        // Accumulate (area-weighted — cross product magnitude IS the area×2)
        chunk.smoothVertices[tri.a].nx += fnx;
        chunk.smoothVertices[tri.a].ny += fny;
        chunk.smoothVertices[tri.a].nz += fnz;
        chunk.smoothVertices[tri.b].nx += fnx;
        chunk.smoothVertices[tri.b].ny += fny;
        chunk.smoothVertices[tri.b].nz += fnz;
        chunk.smoothVertices[tri.c].nx += fnx;
        chunk.smoothVertices[tri.c].ny += fny;
        chunk.smoothVertices[tri.c].nz += fnz;
    }

    // Normalize accumulated vertex normals
    for (auto& sv : chunk.smoothVertices) {
        float len = std::sqrt(sv.nx*sv.nx + sv.ny*sv.ny + sv.nz*sv.nz);
        if (len > 0.0001f) {
            sv.nx /= len; sv.ny /= len; sv.nz /= len;
        } else {
            sv.nx = 0; sv.ny = 1; sv.nz = 0;
        }
    }

    // ── Step 5: Expand indexed triangles to triangle list ─────────
    std::vector<SmoothVertex> expanded;
    expanded.reserve(triangles.size() * 3);
    for (const auto& tri : triangles) {
        expanded.push_back(chunk.smoothVertices[tri.a]);
        expanded.push_back(chunk.smoothVertices[tri.b]);
        expanded.push_back(chunk.smoothVertices[tri.c]);
    }

    chunk.smoothVertices = std::move(expanded);
    chunk.smoothVertexCount = (uint32_t)chunk.smoothVertices.size();

    return chunk.smoothVertexCount;
}

} // namespace voxel
