#include "chunk_mesh.h"

#include "block.h"
#include "chunk.h"
#include "planet.h"
#include "texture_atlas.h"

#include <array>
#include <cmath>
#include <cstdint>

namespace {

// Determine which face index (0..5) is "up" and "down" for the given chunk,
// based on the nearest planet's surface orientation.
// Face encoding: 0=+X 1=-X 2=+Y 3=-Y 4=+Z 5=-Z
void chunkGravityFaces(const ChunkPos& cp, int& upFace, int& downFace) {
    constexpr int N = Chunk::SIZE;
    const float cx = static_cast<float>(cp.x * N + N / 2);
    const float cy = static_cast<float>(cp.y * N + N / 2);
    const float cz = static_cast<float>(cp.z * N + N / 2);

    const auto planets = PlanetManager::planetsNear(cx - N, cy - N, cz - N, cx + N, cy + N, cz + N);

    // Default: Y-up (standard)
    upFace = 2; downFace = 3;

    if (planets.empty()) return;

    // Find planet with greatest Chebyshev depth at chunk centre
    float bestDepth = -1e30f;
    const Planet* best = nullptr;
    for (const auto& p : planets) {
        const float ax = std::abs(cx - p.cx);
        const float ay = std::abs(cy - p.cy);
        const float az = std::abs(cz - p.cz);
        const float cheby = (ax > ay && ax > az) ? ax : (ay > az) ? ay : az;
        const float depth = static_cast<float>(p.baseRadius) - cheby;
        if (depth > bestDepth) { bestDepth = depth; best = &p; }
    }
    if (!best) return;

    const float dx = cx - best->cx, dy = cy - best->cy, dz = cz - best->cz;
    const float ax = std::abs(dx), ay = std::abs(dy), az = std::abs(dz);
    int upAx; float upSgn;
    if (ax >= ay && ax >= az) { upAx = 0; upSgn = dx >= 0.f ? 1.f : -1.f; }
    else if (ay >= az)         { upAx = 1; upSgn = dy >= 0.f ? 1.f : -1.f; }
    else                       { upAx = 2; upSgn = dz >= 0.f ? 1.f : -1.f; }

    upFace   = upAx * 2 + (upSgn > 0.f ? 0 : 1);
    downFace = upAx * 2 + (upSgn > 0.f ? 1 : 0);
}

// Side shading table (per face index, Y-up basis). Reused for any orientation —
// only up/down are remapped; side brightness uses the original face value so
// shading stays stable as the player moves between faces.
static constexpr float kSideShade[6] = { 0.82f, 0.72f, 1.00f, 0.55f, 0.90f, 0.65f };

float faceShade(int face, int upFace, int downFace) {
    if (face == upFace)   return 1.00f;
    if (face == downFace) return 0.55f;
    return kSideShade[face];
}

CubeNetFace netFaceForRenderFace(int face, int upFace, int downFace) {
    if (face == upFace)   return CubeNetFace::Top;
    if (face == downFace) return CubeNetFace::Bottom;
    // Side faces keep their original left/right/front/back mapping
    switch (face) {
        case 0: return CubeNetFace::Right;
        case 1: return CubeNetFace::Left;
        case 2: return CubeNetFace::Top;    // fallback (shouldn't reach if upFace==2)
        case 3: return CubeNetFace::Bottom;
        case 4: return CubeNetFace::Front;
        case 5: return CubeNetFace::Back;
        default: return CubeNetFace::Front;
    }
}

// Returns true if this block occludes AO (solid, not transparent).
bool isOccluder(const World& world, int x, int y, int z) {
    const BlockType t = world.getBlock(x, y, z);
    return isSolid(t) && !isTransparent(t);
}

// Classic AO formula: 0..1 (1 = fully lit, lower = more occluded).
float vertexAO(bool side1, bool side2, bool corner) {
    if (side1 && side2) return 0.46f;
    const int occ = (side1 ? 1 : 0) + (side2 ? 1 : 0) + (corner ? 1 : 0);
    return 1.0f - occ * 0.18f;
}

// AO sample offsets per face per vertex: {side1_dx,dy,dz, side2_dx,dy,dz, corner_dx,dy,dz}
// Each vertex is a corner of the face quad; neighbors are checked in the face plane.
static constexpr int kAONeighbors[6][4][9] = {
    // Face 0 (+x): vertices ordered y-z-, y+z-, y+z+, y-z+
    {{ 1,-1, 0,  1, 0,-1,  1,-1,-1},  { 1, 1, 0,  1, 0,-1,  1, 1,-1},
     { 1, 1, 0,  1, 0, 1,  1, 1, 1},  { 1,-1, 0,  1, 0, 1,  1,-1, 1}},
    // Face 1 (-x): vertices ordered y-z-, y-z+, y+z+, y+z-
    {{-1,-1, 0, -1, 0,-1, -1,-1,-1},  {-1,-1, 0, -1, 0, 1, -1,-1, 1},
     {-1, 1, 0, -1, 0, 1, -1, 1, 1},  {-1, 1, 0, -1, 0,-1, -1, 1,-1}},
    // Face 2 (+y): vertices ordered x-z-, x-z+, x+z+, x+z-
    {{-1, 1, 0,  0, 1,-1, -1, 1,-1},  {-1, 1, 0,  0, 1, 1, -1, 1, 1},
     { 1, 1, 0,  0, 1, 1,  1, 1, 1},  { 1, 1, 0,  0, 1,-1,  1, 1,-1}},
    // Face 3 (-y): vertices ordered x-z-, x+z-, x+z+, x-z+
    {{-1,-1, 0,  0,-1,-1, -1,-1,-1},  { 1,-1, 0,  0,-1,-1,  1,-1,-1},
     { 1,-1, 0,  0,-1, 1,  1,-1, 1},  {-1,-1, 0,  0,-1, 1, -1,-1, 1}},
    // Face 4 (+z): vertices ordered x-y-, x+y-, x+y+, x-y+
    {{-1, 0, 1,  0,-1, 1, -1,-1, 1},  { 1, 0, 1,  0,-1, 1,  1,-1, 1},
     { 1, 0, 1,  0, 1, 1,  1, 1, 1},  {-1, 0, 1,  0, 1, 1, -1, 1, 1}},
    // Face 5 (-z): vertices ordered x-y-, x-y+, x+y+, x+y-
    {{-1, 0,-1,  0,-1,-1, -1,-1,-1},  {-1, 0,-1,  0, 1,-1, -1, 1,-1},
     { 1, 0,-1,  0, 1,-1,  1, 1,-1},  { 1, 0,-1,  0,-1,-1,  1,-1,-1}},
};

bool shouldRenderSolidFace(const World& world, int nx, int ny, int nz) {
    const BlockType neighbor = world.getBlock(nx, ny, nz);
    // Also render face if neighbor is a transparent solid (Leaves, Ice)
    // so blocks behind transparent solids are visible through them.
    return !isSolid(neighbor) || isTransparent(neighbor);
}

bool shouldRenderWaterFace(const World& world, int nx, int ny, int nz, int face, std::uint8_t selfLevel) {
    const BlockType neighbor = world.getBlock(nx, ny, nz);

    if (isSolid(neighbor)) {
        return false;
    }
    if (neighbor == BlockType::Air) {
        return true;
    }
    if (neighbor != BlockType::Water) {
        return true;
    }

    const std::uint8_t neighborLevel = world.getWaterLevel(nx, ny, nz);
    if (face == 2) {
        return neighborLevel < selfLevel;
    }
    if (face == 3) {
        return false;
    }
    return neighborLevel < selfLevel;
}

void addQuad(
    ChunkMeshPart& mesh,
    const std::array<std::array<float, 3>, 4>& verts,
    int face,
    BlockType type,
    const std::array<float, 4>& ao,
    int upFace, int downFace,
    float uvMinU = 0.f, float uvMinV = 0.f,
    float uvMaxU = 1.f, float uvMaxV = 1.f
) {
    static const std::array<std::array<float, 2>, 4> uvA{{{{0.0f, 1.0f}}, {{0.0f, 0.0f}}, {{1.0f, 0.0f}}, {{1.0f, 1.0f}}}};
    static const std::array<std::array<float, 2>, 4> uvB{{{{1.0f, 1.0f}}, {{0.0f, 1.0f}}, {{0.0f, 0.0f}}, {{1.0f, 0.0f}}}};
    static const std::array<std::array<float, 2>, 4> uvC{{{{0.0f, 0.0f}}, {{1.0f, 0.0f}}, {{1.0f, 1.0f}}, {{0.0f, 1.0f}}}};
    static const std::array<std::array<float, 2>, 4> uvD{{{{0.0f, 1.0f}}, {{1.0f, 1.0f}}, {{1.0f, 0.0f}}, {{0.0f, 0.0f}}}};
    const std::array<std::array<float, 2>, 4>* uvs = &uvA;
    if (face == 1 || face == 5) uvs = &uvB;
    if (face == 3) uvs = &uvC;
    if (face == 4) uvs = &uvD;

    const std::uint32_t start = static_cast<std::uint32_t>(mesh.vertices.size());
    const float shade = faceShade(face, upFace, downFace);
    const float texIndex = static_cast<float>(atlasTileIndex(type, netFaceForRenderFace(face, upFace, downFace)));

    for (int i = 0; i < 4; ++i) {
        const float c = shade * ao[i];
        mesh.vertices.push_back(Vertex{
            {verts[i][0], verts[i][1], verts[i][2]},
            {c, c, c},
            {uvMinU + (*uvs)[i][0] * (uvMaxU - uvMinU),
             uvMinV + (*uvs)[i][1] * (uvMaxV - uvMinV)},
            texIndex
        });
    }

    // Flip the quad diagonal to fix AO anisotropy (Minecraft technique).
    if (ao[0] + ao[2] < ao[1] + ao[3]) {
        mesh.indices.push_back(start + 1);
        mesh.indices.push_back(start + 2);
        mesh.indices.push_back(start + 3);
        mesh.indices.push_back(start + 1);
        mesh.indices.push_back(start + 3);
        mesh.indices.push_back(start + 0);
    } else {
        mesh.indices.push_back(start + 0);
        mesh.indices.push_back(start + 1);
        mesh.indices.push_back(start + 2);
        mesh.indices.push_back(start + 0);
        mesh.indices.push_back(start + 2);
        mesh.indices.push_back(start + 3);
    }
}

// Face geometry helper — builds the 4 vertices for a given face.
std::array<std::array<float, 3>, 4> solidFaceVerts(float x, float y, float z, float s, int face) {
    const std::array<std::array<std::array<float, 3>, 4>, 6> verts = {{
        {{{x+s,y,  z  }, {x+s,y+s,z  }, {x+s,y+s,z+s}, {x+s,y,  z+s}}},
        {{{x,  y,  z  }, {x,  y,  z+s}, {x,  y+s,z+s}, {x,  y+s,z  }}},
        {{{x,  y+s,z  }, {x,  y+s,z+s}, {x+s,y+s,z+s}, {x+s,y+s,z  }}},
        {{{x,  y,  z  }, {x+s,y,  z  }, {x+s,y,  z+s}, {x,  y,  z+s}}},
        {{{x,  y,  z+s}, {x+s,y,  z+s}, {x+s,y+s,z+s}, {x,  y+s,z+s}}},
        {{{x,  y,  z  }, {x,  y+s,z  }, {x+s,y+s,z  }, {x+s,y,  z  }}}
    }};
    return verts[face];
}

// No-AO version: used for subdivided micro-voxels and LOD1.
void addSolidFace(ChunkMeshPart& mesh, float x, float y, float z, float s, int face, BlockType type,
    int upFace, int downFace,
    float uvMinU = 0.f, float uvMinV = 0.f, float uvMaxU = 1.f, float uvMaxV = 1.f)
{
    static constexpr std::array<float, 4> kFlatAO = {1.f, 1.f, 1.f, 1.f};
    addQuad(mesh, solidFaceVerts(x, y, z, s, face), face, type, kFlatAO, upFace, downFace, uvMinU, uvMinV, uvMaxU, uvMaxV);
}

// AO version: used for the main LOD0 build loop.
void addSolidFaceAO(ChunkMeshPart& mesh, float x, float y, float z, int face, BlockType type,
    const World& world, int wx, int wy, int wz, int upFace, int downFace)
{
    std::array<float, 4> ao;
    for (int v = 0; v < 4; ++v) {
        const int* n = kAONeighbors[face][v];
        const bool s1 = isOccluder(world, wx + n[0], wy + n[1], wz + n[2]);
        const bool s2 = isOccluder(world, wx + n[3], wy + n[4], wz + n[5]);
        const bool c  = isOccluder(world, wx + n[6], wy + n[7], wz + n[8]);
        ao[v] = vertexAO(s1, s2, c);
    }
    addQuad(mesh, solidFaceVerts(x, y, z, 1.0f, face), face, type, ao, upFace, downFace);
}

BlockType microWorldGet(const World& world, int blockX, int blockY, int blockZ, int microX, int microY, int microZ) {
    while (microX < 0) { microX += MicroVoxelGrid::SIZE; --blockX; }
    while (microY < 0) { microY += MicroVoxelGrid::SIZE; --blockY; }
    while (microZ < 0) { microZ += MicroVoxelGrid::SIZE; --blockZ; }
    while (microX >= MicroVoxelGrid::SIZE) { microX -= MicroVoxelGrid::SIZE; ++blockX; }
    while (microY >= MicroVoxelGrid::SIZE) { microY -= MicroVoxelGrid::SIZE; ++blockY; }
    while (microZ >= MicroVoxelGrid::SIZE) { microZ -= MicroVoxelGrid::SIZE; ++blockZ; }

    const BlockType block = world.getBlock(blockX, blockY, blockZ);
    if (block == BlockType::Air || block == BlockType::Water) {
        return BlockType::Air;
    }
    if (world.isBlockSubdivided(blockX, blockY, blockZ)) {
        return world.getVoxel(blockX, blockY, blockZ, microX, microY, microZ);
    }
    return block;
}

// lx,ly,lz are local chunk coordinates (0..Chunk::SIZE-1); wx,wy,wz are world coords (for voxel lookup).
void buildSubdividedBlock(ChunkMeshPart& mesh, const World& world, int wx, int wy, int wz, int lx, int ly, int lz, int upFace, int downFace) {
    const float s = 1.0f / static_cast<float>(MicroVoxelGrid::SIZE);
    const float N = static_cast<float>(MicroVoxelGrid::SIZE);

    // Seamless mode: when parent block was solid, each voxel samples its own
    // 1/16 slice of the texture so the whole block still looks intact.
    // Standalone mode: parent is Air (user placed voxel), use full texture.
    const bool seamless = (world.getBlock(wx, wy, wz) != BlockType::Air);

    for (int mz = 0; mz < MicroVoxelGrid::SIZE; ++mz) {
        for (int my = 0; my < MicroVoxelGrid::SIZE; ++my) {
            for (int mx = 0; mx < MicroVoxelGrid::SIZE; ++mx) {
                const BlockType type = world.getVoxel(wx, wy, wz, mx, my, mz);
                if (!isSolid(type)) continue;

                // Use LOCAL chunk coords so model matrix handles world offset (no float precision loss).
                const float x = static_cast<float>(lx) + static_cast<float>(mx) * s;
                const float y = static_cast<float>(ly) + static_cast<float>(my) * s;
                const float z = static_cast<float>(lz) + static_cast<float>(mz) * s;

                // UV sub-region per face so adjacent voxels share pixel boundaries.
                // Derivation: for each face, two voxel coords control U and V.
                // face 0 (Right +x): U~z, V~y(inv)
                // face 1 (Left  -x): U~z(inv), V~y(inv)
                // face 2 (Top   +y): U~x, V~z(inv)
                // face 3 (Bot   -y): U~x, V~z(normal)
                // face 4 (Front +z): U~x, V~y(inv)
                // face 5 (Back  -z): U~x(inv), V~y(inv)
                const float fmx = static_cast<float>(mx);
                const float fmy = static_cast<float>(my);
                const float fmz = static_cast<float>(mz);

                auto uvForFace = [&](int face, float& u0, float& v0, float& u1, float& v1) {
                    if (!seamless) { u0 = v0 = 0.f; u1 = v1 = 1.f; return; }
                    switch (face) {
                        case 0: u0=fmz/N;       u1=(fmz+1)/N;   v0=(N-1-fmy)/N; v1=(N-fmy)/N; break;
                        case 1: u0=(N-1-fmz)/N; u1=(N-fmz)/N;   v0=(N-1-fmy)/N; v1=(N-fmy)/N; break;
                        case 2: u0=fmx/N;       u1=(fmx+1)/N;   v0=(N-1-fmz)/N; v1=(N-fmz)/N; break;
                        case 3: u0=fmx/N;       u1=(fmx+1)/N;   v0=fmz/N;       v1=(fmz+1)/N; break;
                        case 4: u0=fmx/N;       u1=(fmx+1)/N;   v0=(N-1-fmy)/N; v1=(N-fmy)/N; break;
                        default:u0=(N-1-fmx)/N; u1=(N-fmx)/N;   v0=(N-1-fmy)/N; v1=(N-fmy)/N; break;
                    }
                };

                float u0, v0, u1, v1;
                if (!isSolid(microWorldGet(world, wx, wy, wz, mx + 1, my, mz))) { uvForFace(0,u0,v0,u1,v1); addSolidFace(mesh, x, y, z, s, 0, type, upFace, downFace, u0, v0, u1, v1); }
                if (!isSolid(microWorldGet(world, wx, wy, wz, mx - 1, my, mz))) { uvForFace(1,u0,v0,u1,v1); addSolidFace(mesh, x, y, z, s, 1, type, upFace, downFace, u0, v0, u1, v1); }
                if (!isSolid(microWorldGet(world, wx, wy, wz, mx, my + 1, mz))) { uvForFace(2,u0,v0,u1,v1); addSolidFace(mesh, x, y, z, s, 2, type, upFace, downFace, u0, v0, u1, v1); }
                if (!isSolid(microWorldGet(world, wx, wy, wz, mx, my - 1, mz))) { uvForFace(3,u0,v0,u1,v1); addSolidFace(mesh, x, y, z, s, 3, type, upFace, downFace, u0, v0, u1, v1); }
                if (!isSolid(microWorldGet(world, wx, wy, wz, mx, my, mz + 1))) { uvForFace(4,u0,v0,u1,v1); addSolidFace(mesh, x, y, z, s, 4, type, upFace, downFace, u0, v0, u1, v1); }
                if (!isSolid(microWorldGet(world, wx, wy, wz, mx, my, mz - 1))) { uvForFace(5,u0,v0,u1,v1); addSolidFace(mesh, x, y, z, s, 5, type, upFace, downFace, u0, v0, u1, v1); }
            }
        }
    }
}

void addWaterFace(ChunkMeshPart& mesh, float x, float y, float z, float waterHeight, int face, int upFace, int downFace) {
    constexpr float edgeInset = 0.01f;
    constexpr float yInsetBottom = 0.01f;
    constexpr float yInsetTop = 0.01f;

    const float x0 = x + edgeInset;
    const float x1 = x + 1.0f - edgeInset;
    const float z0 = z + edgeInset;
    const float z1 = z + 1.0f - edgeInset;

    const float yBottom = y + yInsetBottom;
    const float yTop = std::min(y + waterHeight + yInsetTop, y + 0.98f);

    const std::array<std::array<std::array<float, 3>, 4>, 6> verts = {{
        {{{x1, yBottom, z0}, {x1, yTop, z0}, {x1, yTop, z1}, {x1, yBottom, z1}}},
        {{{x0, yBottom, z0}, {x0, yBottom, z1}, {x0, yTop, z1}, {x0, yTop, z0}}},
        {{{x0, yTop, z0}, {x0, yTop, z1}, {x1, yTop, z1}, {x1, yTop, z0}}},
        {{{x0, yBottom, z0}, {x1, yBottom, z0}, {x1, yBottom, z1}, {x0, yBottom, z1}}},
        {{{x0, yBottom, z1}, {x1, yBottom, z1}, {x1, yTop, z1}, {x0, yTop, z1}}},
        {{{x0, yBottom, z0}, {x0, yTop, z0}, {x1, yTop, z0}, {x1, yBottom, z0}}}
    }};

    const std::uint32_t start = static_cast<std::uint32_t>(mesh.vertices.size());
    const float shade = faceShade(face, upFace, downFace) * 0.95f;
    const float texIndex = static_cast<float>(atlasTileIndex(BlockType::Water, netFaceForRenderFace(face, upFace, downFace)));
    static const std::array<std::array<float, 2>, 4> uvs{{{{0.0f,1.0f}},{{0.0f,0.0f}},{{1.0f,0.0f}},{{1.0f,1.0f}}}};

    for (int i = 0; i < 4; ++i) {
        mesh.vertices.push_back(Vertex{{verts[face][i][0], verts[face][i][1], verts[face][i][2]}, {shade, shade, shade}, {uvs[i][0], uvs[i][1]}, texIndex});
    }

    mesh.indices.push_back(start + 0);
    mesh.indices.push_back(start + 1);
    mesh.indices.push_back(start + 2);
    mesh.indices.push_back(start + 0);
    mesh.indices.push_back(start + 2);
    mesh.indices.push_back(start + 3);
}

// ---------------------------------------------------------------------------
// LOD 1 builder: only the topmost visible block face of each column.
// ---------------------------------------------------------------------------
ChunkMesh buildLod1(const World& world, const ChunkPos& chunkPos) {
    ChunkMesh mesh;

    const Chunk* chunk = world.getChunk(chunkPos);
    if (!chunk) return mesh;

    int upFace, downFace;
    chunkGravityFaces(chunkPos, upFace, downFace);

    // Collect top block info per column
    struct ColTop { int y = -1; BlockType type = BlockType::Air; };
    std::array<std::array<ColTop, Chunk::SIZE>, Chunk::SIZE> tops{};

    for (int lz = 0; lz < Chunk::SIZE; ++lz) {
        for (int lx = 0; lx < Chunk::SIZE; ++lx) {
            for (int ly = Chunk::SIZE - 1; ly >= 0; --ly) {
                BlockType t = chunk->get(lx, ly, lz);
                if (isSolid(t) || t == BlockType::Water) {
                    // Only show top face if the block above is air/transparent
                    const int wy = chunkPos.y * Chunk::SIZE + ly;
                    const int wx = chunkPos.x * Chunk::SIZE + lx;
                    const int wz = chunkPos.z * Chunk::SIZE + lz;
                    const BlockType above = world.getBlock(wx, wy + 1, wz);
                    if (!isSolid(above) && above != BlockType::Water) {
                        tops[lx][lz] = {ly, t};
                    }
                    break;
                }
            }
        }
    }

    // Emit one quad per column (no greedy merge for simplicity at LOD 1).
    // Use LOCAL chunk coords (0..Chunk::SIZE-1) — model matrix handles world offset.
    for (int lz = 0; lz < Chunk::SIZE; ++lz) {
        for (int lx = 0; lx < Chunk::SIZE; ++lx) {
            const ColTop& ct = tops[lx][lz];
            if (ct.y < 0) continue;

            const float fx = static_cast<float>(lx);
            const float fy = static_cast<float>(ct.y);
            const float fz = static_cast<float>(lz);

            if (ct.type == BlockType::Water) {
                addWaterFace(mesh.water, fx, fy, fz, 1.0f, upFace, upFace, downFace);
            } else {
                addSolidFace(mesh.opaque, fx, fy, fz, 1.0f, upFace, ct.type, upFace, downFace);
            }
        }
    }

    return mesh;
}

} // namespace

ChunkMesh ChunkMeshBuilder::build(const World& world, const ChunkPos& chunkPos, int lodLevel) {
    if (lodLevel >= 1) return buildLod1(world, chunkPos);
    ChunkMesh mesh;

    const Chunk* chunk = world.getChunk(chunkPos);
    if (!chunk) {
        return mesh;
    }

    int upFace, downFace;
    chunkGravityFaces(chunkPos, upFace, downFace);

    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 BlockType type = chunk->get(x, y, z);
                const int wx = chunkPos.x * Chunk::SIZE + x;
                const int wy = chunkPos.y * Chunk::SIZE + y;
                const int wz = chunkPos.z * Chunk::SIZE + z;

                if (type == BlockType::Air) {
                    // Air block may still have voxels placed inside it
                    if (world.isBlockSubdivided(wx, wy, wz)) {
                        buildSubdividedBlock(mesh.opaque, world, wx, wy, wz, x, y, z, upFace, downFace);
                    }
                    continue;
                }

                if (type == BlockType::Water) {
                    const std::uint8_t level = chunk->getWaterLevel(x, y, z);
                    if (level == 0) {
                        continue;
                    }

                    const float waterHeight = static_cast<float>(level) / 16.0f;
                    const float lx = static_cast<float>(x), ly = static_cast<float>(y), lz = static_cast<float>(z);
                    if (shouldRenderWaterFace(world, wx + 1, wy, wz, 0, level)) addWaterFace(mesh.water, lx, ly, lz, waterHeight, 0, upFace, downFace);
                    if (shouldRenderWaterFace(world, wx - 1, wy, wz, 1, level)) addWaterFace(mesh.water, lx, ly, lz, waterHeight, 1, upFace, downFace);
                    if (shouldRenderWaterFace(world, wx, wy + 1, wz, 2, level)) addWaterFace(mesh.water, lx, ly, lz, waterHeight, 2, upFace, downFace);
                    if (shouldRenderWaterFace(world, wx, wy - 1, wz, 3, level)) addWaterFace(mesh.water, lx, ly, lz, waterHeight, 3, upFace, downFace);
                    if (shouldRenderWaterFace(world, wx, wy, wz + 1, 4, level)) addWaterFace(mesh.water, lx, ly, lz, waterHeight, 4, upFace, downFace);
                    if (shouldRenderWaterFace(world, wx, wy, wz - 1, 5, level)) addWaterFace(mesh.water, lx, ly, lz, waterHeight, 5, upFace, downFace);
                    continue;
                }

                if (world.isBlockSubdivided(wx, wy, wz)) {
                    buildSubdividedBlock(mesh.opaque, world, wx, wy, wz, x, y, z, upFace, downFace);
                    continue;
                }

                // Use LOCAL chunk coordinates (0..15) — model matrix handles world offset.
                const float lx = static_cast<float>(x), ly = static_cast<float>(y), lz = static_cast<float>(z);
                if (shouldRenderSolidFace(world, wx + 1, wy, wz)) addSolidFaceAO(mesh.opaque, lx, ly, lz, 0, type, world, wx, wy, wz, upFace, downFace);
                if (shouldRenderSolidFace(world, wx - 1, wy, wz)) addSolidFaceAO(mesh.opaque, lx, ly, lz, 1, type, world, wx, wy, wz, upFace, downFace);
                if (shouldRenderSolidFace(world, wx, wy + 1, wz)) addSolidFaceAO(mesh.opaque, lx, ly, lz, 2, type, world, wx, wy, wz, upFace, downFace);
                if (shouldRenderSolidFace(world, wx, wy - 1, wz)) addSolidFaceAO(mesh.opaque, lx, ly, lz, 3, type, world, wx, wy, wz, upFace, downFace);
                if (shouldRenderSolidFace(world, wx, wy, wz + 1)) addSolidFaceAO(mesh.opaque, lx, ly, lz, 4, type, world, wx, wy, wz, upFace, downFace);
                if (shouldRenderSolidFace(world, wx, wy, wz - 1)) addSolidFaceAO(mesh.opaque, lx, ly, lz, 5, type, world, wx, wy, wz, upFace, downFace);
            }
        }
    }

    return mesh;
}
