// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.builtin.buildertools.objimport;

import java.util.List;
import javax.annotation.Nullable;
import com.hypixel.hytale.builtin.buildertools.BlockColorIndex;
import java.awt.image.BufferedImage;
import java.util.Map;
import javax.annotation.Nonnull;

public final class MeshVoxelizer
{
    private MeshVoxelizer() {
    }
    
    @Nonnull
    public static VoxelResult voxelize(@Nonnull final ObjParser.ObjMesh mesh, final int targetHeight, final boolean fillSolid) {
        return voxelize(mesh, targetHeight, fillSolid, null, null, null, 0, false);
    }
    
    @Nonnull
    public static VoxelResult voxelize(@Nonnull final ObjParser.ObjMesh mesh, final int targetHeight, final boolean fillSolid, @Nullable final Map<String, Integer> materialToBlockId) {
        return voxelize(mesh, targetHeight, fillSolid, null, materialToBlockId, null, 0, false);
    }
    
    @Nonnull
    public static VoxelResult voxelize(@Nonnull final ObjParser.ObjMesh mesh, final int targetHeight, final boolean fillSolid, @Nullable final Map<String, Integer> materialToBlockId, final int defaultBlockId) {
        return voxelize(mesh, targetHeight, fillSolid, null, materialToBlockId, null, defaultBlockId, false);
    }
    
    @Nonnull
    public static VoxelResult voxelize(@Nonnull final ObjParser.ObjMesh mesh, final int targetHeight, final boolean fillSolid, @Nullable final Map<String, BufferedImage> materialTextures, @Nullable final Map<String, Integer> materialToBlockId, @Nullable final BlockColorIndex colorIndex, final int defaultBlockId) {
        return voxelize(mesh, targetHeight, fillSolid, materialTextures, materialToBlockId, colorIndex, defaultBlockId, false);
    }
    
    @Nonnull
    public static VoxelResult voxelize(@Nonnull final ObjParser.ObjMesh mesh, final int targetHeight, final boolean fillSolid, @Nullable final Map<String, BufferedImage> materialTextures, @Nullable final Map<String, Integer> materialToBlockId, @Nullable final BlockColorIndex colorIndex, final int defaultBlockId, final boolean preserveOrigin) {
        final float[] bounds = mesh.getBounds();
        final float meshHeight = bounds[4] - bounds[1];
        final float meshWidth = bounds[3] - bounds[0];
        final float meshDepth = bounds[5] - bounds[2];
        if (meshHeight <= 0.0f) {
            return new VoxelResult(new boolean[1][1][1], null, 1, 1, 1);
        }
        final float scale = targetHeight / meshHeight;
        final float[][] scaledVertices = new float[mesh.vertices().size()][3];
        int sizeX;
        int sizeY;
        int sizeZ;
        if (preserveOrigin) {
            final float scaledMinX = bounds[0] * scale;
            final float scaledMaxX = bounds[3] * scale;
            final float scaledMinY = bounds[1] * scale;
            final float scaledMaxY = bounds[4] * scale;
            final float scaledMinZ = bounds[2] * scale;
            final float scaledMaxZ = bounds[5] * scale;
            final float offsetX = (scaledMinX < 0.0f) ? (-scaledMinX + 1.0f) : 1.0f;
            final float offsetY = (scaledMinY < 0.0f) ? (-scaledMinY + 1.0f) : 1.0f;
            final float offsetZ = (scaledMinZ < 0.0f) ? (-scaledMinZ + 1.0f) : 1.0f;
            for (int i = 0; i < mesh.vertices().size(); ++i) {
                final float[] v = mesh.vertices().get(i);
                scaledVertices[i][0] = v[0] * scale + offsetX;
                scaledVertices[i][1] = v[1] * scale + offsetY;
                scaledVertices[i][2] = v[2] * scale + offsetZ;
            }
            sizeX = Math.max(1, (int)Math.ceil(scaledMaxX + offsetX)) + 2;
            sizeY = Math.max(1, (int)Math.ceil(scaledMaxY + offsetY)) + 2;
            sizeZ = Math.max(1, (int)Math.ceil(scaledMaxZ + offsetZ)) + 2;
        }
        else {
            sizeX = Math.max(1, (int)Math.ceil(meshWidth * scale)) + 2;
            sizeY = Math.max(1, targetHeight) + 2;
            sizeZ = Math.max(1, (int)Math.ceil(meshDepth * scale)) + 2;
            for (int j = 0; j < mesh.vertices().size(); ++j) {
                final float[] v2 = mesh.vertices().get(j);
                scaledVertices[j][0] = (v2[0] - bounds[0]) * scale + 1.0f;
                scaledVertices[j][1] = (v2[1] - bounds[1]) * scale + 1.0f;
                scaledVertices[j][2] = (v2[2] - bounds[2]) * scale + 1.0f;
            }
        }
        final boolean[][][] shell = new boolean[sizeX][sizeY][sizeZ];
        final boolean hasTextures = materialTextures != null && !materialTextures.isEmpty() && colorIndex != null;
        final int[][][] blockIds = (int[][][])((hasTextures || materialToBlockId != null || defaultBlockId != 0) ? new int[sizeX][sizeY][sizeZ] : null);
        rasterizeSurface(shell, blockIds, scaledVertices, mesh, materialTextures, materialToBlockId, colorIndex, defaultBlockId, sizeX, sizeY, sizeZ);
        if (fillSolid) {
            final boolean[][][] solid = floodFillSolid(shell, sizeX, sizeY, sizeZ);
            if (blockIds != null) {
                fillInteriorBlockIds(solid, shell, blockIds, defaultBlockId, sizeX, sizeY, sizeZ);
            }
            return cropToSolidBounds(solid, blockIds, sizeX, sizeY, sizeZ);
        }
        return cropToSolidBounds(shell, blockIds, sizeX, sizeY, sizeZ);
    }
    
    private static int resolveIndex(final int index, final int count) {
        return (index < 0) ? (count + index) : index;
    }
    
    private static void rasterizeSurface(final boolean[][][] voxels, @Nullable final int[][][] blockIds, final float[][] vertices, final ObjParser.ObjMesh mesh, @Nullable final Map<String, BufferedImage> materialTextures, @Nullable final Map<String, Integer> materialToBlockId, @Nullable final BlockColorIndex colorIndex, final int defaultBlockId, final int sizeX, final int sizeY, final int sizeZ) {
        final List<int[]> faces = mesh.faces();
        final List<int[]> faceUvIndices = mesh.faceUvIndices();
        final List<float[]> uvCoordinates = mesh.uvCoordinates();
        final List<String> faceMaterials = mesh.faceMaterials();
        final boolean hasTextures = materialTextures != null && !materialTextures.isEmpty() && colorIndex != null;
        for (int faceIdx = 0; faceIdx < faces.size(); ++faceIdx) {
            final int[] face = faces.get(faceIdx);
            final int i0 = resolveIndex(face[0], vertices.length);
            final int i2 = resolveIndex(face[1], vertices.length);
            final int i3 = resolveIndex(face[2], vertices.length);
            final float[] v0 = vertices[i0];
            final float[] v2 = vertices[i2];
            final float[] v3 = vertices[i3];
            final String material = (faceIdx < faceMaterials.size()) ? faceMaterials.get(faceIdx) : null;
            BufferedImage texture = null;
            int faceBlockId = defaultBlockId;
            if (material != null) {
                if (hasTextures) {
                    texture = materialTextures.get(material);
                }
                if (texture == null && materialToBlockId != null) {
                    faceBlockId = materialToBlockId.getOrDefault(material, defaultBlockId);
                }
            }
            float[] uv0 = null;
            float[] uv2 = null;
            float[] uv3 = null;
            if (texture != null && faceIdx < faceUvIndices.size()) {
                final int[] uvIndices = faceUvIndices.get(faceIdx);
                if (uvIndices != null && uvIndices.length >= 3) {
                    final int uvCount = uvCoordinates.size();
                    final int ui0 = resolveIndex(uvIndices[0], uvCount);
                    final int ui2 = resolveIndex(uvIndices[1], uvCount);
                    final int ui3 = resolveIndex(uvIndices[2], uvCount);
                    if (ui0 >= 0 && ui0 < uvCount) {
                        uv0 = uvCoordinates.get(ui0);
                    }
                    if (ui2 >= 0 && ui2 < uvCount) {
                        uv2 = uvCoordinates.get(ui2);
                    }
                    if (ui3 >= 0 && ui3 < uvCount) {
                        uv3 = uvCoordinates.get(ui3);
                    }
                }
            }
            rasterizeLine(voxels, blockIds, v0, v2, uv0, uv2, texture, colorIndex, faceBlockId, sizeX, sizeY, sizeZ);
            rasterizeLine(voxels, blockIds, v2, v3, uv2, uv3, texture, colorIndex, faceBlockId, sizeX, sizeY, sizeZ);
            rasterizeLine(voxels, blockIds, v3, v0, uv3, uv0, texture, colorIndex, faceBlockId, sizeX, sizeY, sizeZ);
            rasterizeTriangle(voxels, blockIds, v0, v2, v3, uv0, uv2, uv3, texture, colorIndex, faceBlockId, sizeX, sizeY, sizeZ);
        }
    }
    
    private static void rasterizeLine(final boolean[][][] voxels, @Nullable final int[][][] blockIds, final float[] a, final float[] b, @Nullable final float[] uvA, @Nullable final float[] uvB, @Nullable final BufferedImage texture, @Nullable final BlockColorIndex colorIndex, final int fallbackBlockId, final int sizeX, final int sizeY, final int sizeZ) {
        final float dx = b[0] - a[0];
        final float dy = b[1] - a[1];
        final float dz = b[2] - a[2];
        final float len = (float)Math.sqrt(dx * dx + dy * dy + dz * dz);
        if (len < 0.001f) {
            final int blockId = sampleBlockId(uvA, texture, colorIndex, fallbackBlockId);
            setVoxel(voxels, blockIds, (int)a[0], (int)a[1], (int)a[2], blockId, sizeX, sizeY, sizeZ);
            return;
        }
        for (int steps = (int)Math.ceil(len * 2.0f) + 1, i = 0; i <= steps; ++i) {
            final float t = i / (float)steps;
            final float x = a[0] + dx * t;
            final float y = a[1] + dy * t;
            final float z = a[2] + dz * t;
            final float[] uv = interpolateUv(uvA, uvB, t);
            final int blockId2 = sampleBlockId(uv, texture, colorIndex, fallbackBlockId);
            setVoxel(voxels, blockIds, (int)x, (int)y, (int)z, blockId2, sizeX, sizeY, sizeZ);
        }
    }
    
    @Nullable
    private static float[] interpolateUv(@Nullable final float[] uvA, @Nullable final float[] uvB, final float t) {
        if (uvA == null || uvB == null) {
            return uvA;
        }
        return new float[] { uvA[0] + (uvB[0] - uvA[0]) * t, uvA[1] + (uvB[1] - uvA[1]) * t };
    }
    
    private static int sampleBlockId(@Nullable final float[] uv, @Nullable final BufferedImage texture, @Nullable final BlockColorIndex colorIndex, final int fallbackBlockId) {
        if (uv == null || texture == null || colorIndex == null) {
            return fallbackBlockId;
        }
        final int alpha = TextureSampler.sampleAlphaAt(texture, uv[0], uv[1]);
        if (alpha < 128) {
            return 0;
        }
        final int[] rgb = TextureSampler.sampleAt(texture, uv[0], uv[1]);
        final int blockId = colorIndex.findClosestBlock(rgb[0], rgb[1], rgb[2]);
        return (blockId > 0) ? blockId : fallbackBlockId;
    }
    
    private static void setVoxel(final boolean[][][] voxels, @Nullable final int[][][] blockIds, final int x, final int y, final int z, final int blockId, final int sizeX, final int sizeY, final int sizeZ) {
        if (x >= 0 && x < sizeX && y >= 0 && y < sizeY && z >= 0 && z < sizeZ) {
            voxels[x][y][z] = true;
            if (blockIds != null && blockId != 0 && blockIds[x][y][z] == 0) {
                blockIds[x][y][z] = blockId;
            }
        }
    }
    
    private static void rasterizeTriangle(final boolean[][][] voxels, @Nullable final int[][][] blockIds, final float[] v0, final float[] v1, final float[] v2, @Nullable final float[] uv0, @Nullable final float[] uv1, @Nullable final float[] uv2, @Nullable final BufferedImage texture, @Nullable final BlockColorIndex colorIndex, final int fallbackBlockId, final int sizeX, final int sizeY, final int sizeZ) {
        final float minX = Math.min(v0[0], Math.min(v1[0], v2[0]));
        final float maxX = Math.max(v0[0], Math.max(v1[0], v2[0]));
        final float minY = Math.min(v0[1], Math.min(v1[1], v2[1]));
        final float maxY = Math.max(v0[1], Math.max(v1[1], v2[1]));
        final float minZ = Math.min(v0[2], Math.min(v1[2], v2[2]));
        final float maxZ = Math.max(v0[2], Math.max(v1[2], v2[2]));
        final int startX = Math.max(0, (int)Math.floor(minX) - 1);
        final int endX = Math.min(sizeX - 1, (int)Math.ceil(maxX) + 1);
        final int startY = Math.max(0, (int)Math.floor(minY) - 1);
        final int endY = Math.min(sizeY - 1, (int)Math.ceil(maxY) + 1);
        final int startZ = Math.max(0, (int)Math.floor(minZ) - 1);
        final int endZ = Math.min(sizeZ - 1, (int)Math.ceil(maxZ) + 1);
        final boolean hasUvSampling = uv0 != null && uv1 != null && uv2 != null && texture != null && colorIndex != null;
        for (int x = startX; x <= endX; ++x) {
            for (int y = startY; y <= endY; ++y) {
                for (int z = startZ; z <= endZ; ++z) {
                    final float px = x + 0.5f;
                    final float py = y + 0.5f;
                    final float pz = z + 0.5f;
                    if (pointNearTriangle(px, py, pz, v0, v1, v2, 0.87f)) {
                        int blockId = fallbackBlockId;
                        if (hasUvSampling) {
                            final float[] bary = barycentric(px, py, pz, v0, v1, v2);
                            if (bary != null) {
                                final float u = bary[0] * uv0[0] + bary[1] * uv1[0] + bary[2] * uv2[0];
                                final float v3 = bary[0] * uv0[1] + bary[1] * uv1[1] + bary[2] * uv2[1];
                                final int alpha = TextureSampler.sampleAlphaAt(texture, u, v3);
                                if (alpha < 128) {
                                    continue;
                                }
                                final int[] rgb = TextureSampler.sampleAt(texture, u, v3);
                                final int sampledId = colorIndex.findClosestBlock(rgb[0], rgb[1], rgb[2]);
                                if (sampledId > 0) {
                                    blockId = sampledId;
                                }
                            }
                        }
                        voxels[x][y][z] = true;
                        if (blockIds != null && blockId != 0 && blockIds[x][y][z] == 0) {
                            blockIds[x][y][z] = blockId;
                        }
                    }
                }
            }
        }
    }
    
    @Nullable
    private static float[] barycentric(final float px, final float py, final float pz, final float[] v0, final float[] v1, final float[] v2) {
        final float[] e1 = { v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2] };
        final float[] e2 = { v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2] };
        final float nx = e1[1] * e2[2] - e1[2] * e2[1];
        final float ny = e1[2] * e2[0] - e1[0] * e2[2];
        final float nz = e1[0] * e2[1] - e1[1] * e2[0];
        final float ax = Math.abs(nx);
        final float ay = Math.abs(ny);
        final float az = Math.abs(nz);
        float u0;
        float v0c;
        float u2;
        float v1c;
        float u3;
        float v2c;
        float pu;
        float pv;
        if (ax >= ay && ax >= az) {
            u0 = v0[1];
            v0c = v0[2];
            u2 = v1[1];
            v1c = v1[2];
            u3 = v2[1];
            v2c = v2[2];
            pu = py;
            pv = pz;
        }
        else if (ay >= ax && ay >= az) {
            u0 = v0[0];
            v0c = v0[2];
            u2 = v1[0];
            v1c = v1[2];
            u3 = v2[0];
            v2c = v2[2];
            pu = px;
            pv = pz;
        }
        else {
            u0 = v0[0];
            v0c = v0[1];
            u2 = v1[0];
            v1c = v1[1];
            u3 = v2[0];
            v2c = v2[1];
            pu = px;
            pv = py;
        }
        final float denom = (v1c - v2c) * (u0 - u3) + (u3 - u2) * (v0c - v2c);
        if (Math.abs(denom) < 1.0E-10f) {
            return null;
        }
        final float w0 = ((v1c - v2c) * (pu - u3) + (u3 - u2) * (pv - v2c)) / denom;
        final float w2 = ((v2c - v0c) * (pu - u3) + (u0 - u3) * (pv - v2c)) / denom;
        final float w3 = 1.0f - w0 - w2;
        return new float[] { w0, w2, w3 };
    }
    
    private static boolean pointNearTriangle(final float px, final float py, final float pz, final float[] v0, final float[] v1, final float[] v2, final float threshold) {
        final float e1x = v1[0] - v0[0];
        final float e1y = v1[1] - v0[1];
        final float e1z = v1[2] - v0[2];
        final float e2x = v2[0] - v0[0];
        final float e2y = v2[1] - v0[1];
        final float e2z = v2[2] - v0[2];
        final float nx = e1y * e2z - e1z * e2y;
        final float ny = e1z * e2x - e1x * e2z;
        final float nz = e1x * e2y - e1y * e2x;
        final float lenSq = nx * nx + ny * ny + nz * nz;
        if (lenSq < 1.0E-12f) {
            return false;
        }
        final float len = (float)Math.sqrt(lenSq);
        final float dpx = px - v0[0];
        final float dpy = py - v0[1];
        final float dpz = pz - v0[2];
        final float dotNP = nx * dpx + ny * dpy + nz * dpz;
        final float dist = Math.abs(dotNP) / len;
        if (dist > threshold) {
            return false;
        }
        final float t = dotNP / lenSq;
        final float projX = px - t * nx;
        final float projY = py - t * ny;
        final float projZ = pz - t * nz;
        return pointInTriangleWithTolerance(projX, projY, projZ, v0, v1, v2, 0.1f);
    }
    
    private static boolean pointInTriangleWithTolerance(final float px, final float py, final float pz, final float[] v0, final float[] v1, final float[] v2, final float tolerance) {
        final float vax = v1[0] - v0[0];
        final float vay = v1[1] - v0[1];
        final float vaz = v1[2] - v0[2];
        final float vbx = v2[0] - v0[0];
        final float vby = v2[1] - v0[1];
        final float vbz = v2[2] - v0[2];
        final float vpx = px - v0[0];
        final float vpy = py - v0[1];
        final float vpz = pz - v0[2];
        final float d00 = vax * vax + vay * vay + vaz * vaz;
        final float d2 = vax * vbx + vay * vby + vaz * vbz;
        final float d3 = vbx * vbx + vby * vby + vbz * vbz;
        final float d4 = vpx * vax + vpy * vay + vpz * vaz;
        final float d5 = vpx * vbx + vpy * vby + vpz * vbz;
        final float denom = d00 * d3 - d2 * d2;
        if (Math.abs(denom) < 1.0E-12f) {
            return false;
        }
        final float u = (d3 * d4 - d2 * d5) / denom;
        final float v3 = (d00 * d5 - d2 * d4) / denom;
        return u >= -tolerance && v3 >= -tolerance && u + v3 <= 1.0f + tolerance;
    }
    
    private static boolean[][][] floodFillSolid(final boolean[][][] shell, final int sizeX, final int sizeY, final int sizeZ) {
        final int dx = sizeX + 2;
        final int dy = sizeY + 2;
        final int dz = sizeZ + 2;
        final int plane = dx * dy;
        final int total = plane * dz;
        final boolean[] visited = new boolean[total];
        final int[] queue = new int[total];
        int qh = 0;
        int qt = 0;
        visited[0] = true;
        queue[qt++] = 0;
        while (qh < qt) {
            final int idx = queue[qh++];
            final int x = idx % dx;
            final int y = idx / dx % dy;
            final int z = idx / plane;
            if (x + 1 < dx && tryEnqueue(shell, sizeX, sizeY, sizeZ, visited, queue, x + 1, y, z, dx, plane, qt)) {
                ++qt;
            }
            if (x - 1 >= 0 && tryEnqueue(shell, sizeX, sizeY, sizeZ, visited, queue, x - 1, y, z, dx, plane, qt)) {
                ++qt;
            }
            if (y + 1 < dy && tryEnqueue(shell, sizeX, sizeY, sizeZ, visited, queue, x, y + 1, z, dx, plane, qt)) {
                ++qt;
            }
            if (y - 1 >= 0 && tryEnqueue(shell, sizeX, sizeY, sizeZ, visited, queue, x, y - 1, z, dx, plane, qt)) {
                ++qt;
            }
            if (z + 1 < dz && tryEnqueue(shell, sizeX, sizeY, sizeZ, visited, queue, x, y, z + 1, dx, plane, qt)) {
                ++qt;
            }
            if (z - 1 >= 0 && tryEnqueue(shell, sizeX, sizeY, sizeZ, visited, queue, x, y, z - 1, dx, plane, qt)) {
                ++qt;
            }
        }
        final boolean[][][] solid = new boolean[sizeX][sizeY][sizeZ];
        for (int x = 0; x < sizeX; ++x) {
            for (int y = 0; y < sizeY; ++y) {
                for (int z = 0; z < sizeZ; ++z) {
                    final int ex = x + 1;
                    final int ey = y + 1;
                    final int ez = z + 1;
                    final int eIdx = ex + ey * dx + ez * plane;
                    solid[x][y][z] = !visited[eIdx];
                }
            }
        }
        return solid;
    }
    
    private static boolean tryEnqueue(final boolean[][][] shell, final int sizeX, final int sizeY, final int sizeZ, final boolean[] visited, final int[] queue, final int ex, final int ey, final int ez, final int dx, final int plane, final int writeIndex) {
        final int idx = ex + ey * dx + ez * plane;
        if (visited[idx]) {
            return false;
        }
        final int x = ex - 1;
        final int y = ey - 1;
        final int z = ez - 1;
        if (x >= 0 && y >= 0 && z >= 0 && x < sizeX && y < sizeY && z < sizeZ && shell[x][y][z]) {
            return false;
        }
        visited[idx] = true;
        queue[writeIndex] = idx;
        return true;
    }
    
    private static VoxelResult cropToSolidBounds(final boolean[][][] voxels, @Nullable final int[][][] blockIds, final int sizeX, final int sizeY, final int sizeZ) {
        int minX = sizeX;
        int minY = sizeY;
        int minZ = sizeZ;
        int maxX = -1;
        int maxY = -1;
        int maxZ = -1;
        for (int x = 0; x < sizeX; ++x) {
            for (int y = 0; y < sizeY; ++y) {
                for (int z = 0; z < sizeZ; ++z) {
                    if (voxels[x][y][z]) {
                        if (x < minX) {
                            minX = x;
                        }
                        if (y < minY) {
                            minY = y;
                        }
                        if (z < minZ) {
                            minZ = z;
                        }
                        if (x > maxX) {
                            maxX = x;
                        }
                        if (y > maxY) {
                            maxY = y;
                        }
                        if (z > maxZ) {
                            maxZ = z;
                        }
                    }
                }
            }
        }
        if (maxX < minX || maxY < minY || maxZ < minZ) {
            return new VoxelResult(new boolean[1][1][1], null, 1, 1, 1);
        }
        final int outX = maxX - minX + 1;
        final int outY = maxY - minY + 1;
        final int outZ = maxZ - minZ + 1;
        final boolean[][][] out = new boolean[outX][outY][outZ];
        final int[][][] outBlockIds = (int[][][])((blockIds != null) ? new int[outX][outY][outZ] : null);
        for (int x2 = 0; x2 < outX; ++x2) {
            for (int y2 = 0; y2 < outY; ++y2) {
                System.arraycopy(voxels[minX + x2][minY + y2], minZ, out[x2][y2], 0, outZ);
                if (outBlockIds != null && blockIds != null) {
                    System.arraycopy(blockIds[minX + x2][minY + y2], minZ, outBlockIds[x2][y2], 0, outZ);
                }
            }
        }
        return new VoxelResult(out, outBlockIds, outX, outY, outZ);
    }
    
    private static void fillInteriorBlockIds(final boolean[][][] solid, final boolean[][][] shell, final int[][][] blockIds, final int defaultBlockId, final int sizeX, final int sizeY, final int sizeZ) {
        for (int x = 0; x < sizeX; ++x) {
            for (int y = 0; y < sizeY; ++y) {
                for (int z = 0; z < sizeZ; ++z) {
                    if (solid[x][y][z] && !shell[x][y][z] && blockIds[x][y][z] == 0) {
                        final int bestId = findNearestSurfaceBlockId(blockIds, shell, x, y, z, sizeX, sizeY, sizeZ);
                        blockIds[x][y][z] = ((bestId != 0) ? bestId : defaultBlockId);
                    }
                }
            }
        }
    }
    
    private static int findNearestSurfaceBlockId(final int[][][] blockIds, final boolean[][][] shell, final int cx, final int cy, final int cz, final int sizeX, final int sizeY, final int sizeZ) {
        for (int radius = 1; radius <= 5; ++radius) {
            for (int dx = -radius; dx <= radius; ++dx) {
                for (int dy = -radius; dy <= radius; ++dy) {
                    for (int dz = -radius; dz <= radius; ++dz) {
                        final int nx = cx + dx;
                        final int ny = cy + dy;
                        final int nz = cz + dz;
                        if (nx >= 0 && nx < sizeX && ny >= 0 && ny < sizeY && nz >= 0 && nz < sizeZ && shell[nx][ny][nz] && blockIds[nx][ny][nz] != 0) {
                            return blockIds[nx][ny][nz];
                        }
                    }
                }
            }
        }
        return 0;
    }
    
    record VoxelResult(boolean[][][] voxels, @Nullable int[][][] blockIds, int sizeX, int sizeY, int sizeZ) {
        public int countSolid() {
            int count = 0;
            for (int x = 0; x < this.sizeX; ++x) {
                for (int y = 0; y < this.sizeY; ++y) {
                    for (int z = 0; z < this.sizeZ; ++z) {
                        if (this.voxels[x][y][z]) {
                            ++count;
                        }
                    }
                }
            }
            return count;
        }
        
        public int getBlockId(final int x, final int y, final int z) {
            if (this.blockIds == null) {
                return 0;
            }
            if (x < 0 || x >= this.sizeX || y < 0 || y >= this.sizeY || z < 0 || z >= this.sizeZ) {
                return 0;
            }
            return this.blockIds[x][y][z];
        }
        
        @Nullable
        public int[][][] blockIds() {
            return this.blockIds;
        }
    }
}
