"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertTexturedBoxesToVoxel = convertTexturedBoxesToVoxel;
const textures_1 = require("../../textures");
const FACE_ORDER = ['north', 'south', 'east', 'west', 'top', 'bottom'];
const EPSILON = 1e-6;
const toNumber = (value) => {
    const num = Number(value);
    return Number.isFinite(num) ? num : 0;
};
const round = (value) => Math.round(value * 1e6) / 1e6;
const FACE_DIMENSIONS = {
    north: { widthAxis: 'x', heightAxis: 'y' },
    south: { widthAxis: 'x', heightAxis: 'y' },
    east: { widthAxis: 'z', heightAxis: 'y' },
    west: { widthAxis: 'z', heightAxis: 'y' },
    top: { widthAxis: 'x', heightAxis: 'z' },
    bottom: { widthAxis: 'x', heightAxis: 'z' },
};
const mapFaceCoordinate = (face, u, v, dims, offsets) => {
    switch (face) {
        case 'north':
            return [offsets.x + u, dims.y - 1 - offsets.y - v, dims.z - 1];
        case 'south':
            return [dims.x - 1 - offsets.x - u, dims.y - 1 - offsets.y - v, 0];
        case 'east':
            return [dims.x - 1, dims.y - 1 - offsets.y - v, dims.z - 1 - offsets.z - u];
        case 'west':
            return [0, dims.y - 1 - offsets.y - v, offsets.z + u];
        case 'top':
            return [offsets.x + u, dims.y - 1, offsets.z + v];
        case 'bottom':
            return [offsets.x + u, 0, dims.z - 1 - offsets.z - v];
        default:
            return null;
    }
};
const isTransparent = (material) => {
    if (!material) {
        return true;
    }
    const opacity = material.opacity === undefined ? 1 : material.opacity;
    return Number(opacity) <= EPSILON;
};
const gatherPaletteIndex = (materialId, paletteIndex, palette) => {
    let index = paletteIndex.get(materialId);
    if (index === undefined) {
        index = palette.length;
        paletteIndex.set(materialId, index);
        palette.push(materialId);
    }
    return index;
};
const createVoxelVolume = (dims) => {
    const total = dims.x * dims.y * dims.z;
    return {
        dims,
        voxels: new Array(total).fill(0),
        palette: [],
        paletteIndex: new Map(),
    };
};
const paintPrimitiveIntoVolume = (primitive, textureLookup, patternLookup, materialLookup, patternDims, offsets, volume, translation = { x: 0, y: 0, z: 0 }, sampleOffset = { u: 0, v: 0 }) => {
    FACE_ORDER.forEach(face => {
        const textureId = (0, textures_1.resolveFaceTextureId)(primitive, face);
        if (!textureId) {
            return;
        }
        const texture = textureLookup.get(textureId);
        if (!texture) {
            return;
        }
        const pattern = patternLookup.get(texture.pattern);
        if (!pattern || !Array.isArray(pattern.data)) {
            return;
        }
        const patternWidth = Math.max(1, Math.round(toNumber(pattern.size?.[0])));
        const patternHeight = Math.max(1, Math.round(toNumber(pattern.size?.[1])));
        const widthAxis = FACE_DIMENSIONS[face].widthAxis;
        const heightAxis = FACE_DIMENSIONS[face].heightAxis;
        const width = Math.max(1, Math.round(patternDims[widthAxis] - offsets[widthAxis] * 2));
        const height = Math.max(1, Math.round(patternDims[heightAxis] - offsets[heightAxis] * 2));
        if (patternWidth < width + sampleOffset.u || patternHeight < height + sampleOffset.v) {
            throw new Error(`Pattern ${pattern.id} for face ${face} has size ${patternWidth}x${patternHeight} but expected at least ${width + sampleOffset.u}x${height + sampleOffset.v}`);
        }
        for (let v = 0; v < height; v += 1) {
            for (let u = 0; u < width; u += 1) {
                const sampleU = u + sampleOffset.u;
                const sampleV = v + sampleOffset.v;
                const patternIndex = sampleV * patternWidth + sampleU;
                const paletteRef = pattern.data[patternIndex] ?? 0;
                const materialId = texture.materials[paletteRef];
                if (!materialId) {
                    continue;
                }
                const material = materialLookup.get(materialId);
                if (isTransparent(material)) {
                    continue;
                }
                const coords = mapFaceCoordinate(face, u, v, patternDims, offsets);
                if (!coords) {
                    continue;
                }
                const x = coords[0] + translation.x;
                const y = coords[1] + translation.y;
                const z = coords[2] + translation.z;
                if (x < 0 ||
                    x >= volume.dims.x ||
                    y < 0 ||
                    y >= volume.dims.y ||
                    z < 0 ||
                    z >= volume.dims.z) {
                    continue;
                }
                const voxelIndex = x + volume.dims.x * (y + volume.dims.y * z);
                if (volume.voxels[voxelIndex] !== 0) {
                    continue;
                }
                const index = gatherPaletteIndex(materialId, volume.paletteIndex, volume.palette);
                volume.voxels[voxelIndex] = index + 1;
            }
        }
    });
};
const computeTargetSize = (source, expansion) => {
    const base = [
        round(toNumber(source?.[0])),
        round(toNumber(source?.[1])),
        round(toNumber(source?.[2])),
    ];
    return [
        Math.max(1, Math.round(base[0] + expansion)),
        Math.max(1, Math.round(base[1] + expansion)),
        Math.max(1, Math.round(base[2] + expansion)),
    ];
};
const axisDimsFromSize = (size) => ({
    x: Math.max(1, Math.round(toNumber(size?.[0]))),
    y: Math.max(1, Math.round(toNumber(size?.[1]))),
    z: Math.max(1, Math.round(toNumber(size?.[2]))),
});
const clampOffsetsToDims = (offsets, dims) => ({
    x: Math.min(Math.max(offsets.x, 0), dims.x),
    y: Math.min(Math.max(offsets.y, 0), dims.y),
    z: Math.min(Math.max(offsets.z, 0), dims.z),
});
const neighborOffsets = [
    [1, 0, 0],
    [-1, 0, 0],
    [0, 1, 0],
    [0, -1, 0],
    [0, 0, 1],
    [0, 0, -1],
];
const indexFromCoords = (x, y, z, dims) => x + dims.x * (y + dims.y * z);
const fillInteriorVoxels = (volume) => {
    const { dims, voxels } = volume;
    const total = voxels.length;
    if (total === 0) {
        return;
    }
    const exterior = new Uint8Array(total);
    const queue = [];
    const pushExterior = (x, y, z) => {
        if (x < 0 || y < 0 || z < 0 || x >= dims.x || y >= dims.y || z >= dims.z) {
            return;
        }
        const idx = indexFromCoords(x, y, z, dims);
        if (exterior[idx] !== 0 || voxels[idx] !== 0) {
            return;
        }
        exterior[idx] = 1;
        queue.push(idx);
    };
    for (let x = 0; x < dims.x; x += 1) {
        for (let y = 0; y < dims.y; y += 1) {
            pushExterior(x, y, 0);
            pushExterior(x, y, dims.z - 1);
        }
    }
    for (let x = 0; x < dims.x; x += 1) {
        for (let z = 0; z < dims.z; z += 1) {
            pushExterior(x, 0, z);
            pushExterior(x, dims.y - 1, z);
        }
    }
    for (let y = 0; y < dims.y; y += 1) {
        for (let z = 0; z < dims.z; z += 1) {
            pushExterior(0, y, z);
            pushExterior(dims.x - 1, y, z);
        }
    }
    while (queue.length > 0) {
        const idx = queue.shift();
        const x = idx % dims.x;
        const yz = (idx - x) / dims.x;
        const y = yz % dims.y;
        const z = Math.floor(yz / dims.y);
        for (const [dx, dy, dz] of neighborOffsets) {
            pushExterior(x + dx, y + dy, z + dz);
        }
    }
    const fillQueue = [];
    for (let idx = 0; idx < total; idx += 1) {
        if (voxels[idx] !== 0) {
            fillQueue.push(idx);
        }
    }
    while (fillQueue.length > 0) {
        const idx = fillQueue.shift();
        const value = voxels[idx];
        const x = idx % dims.x;
        const yz = (idx - x) / dims.x;
        const y = yz % dims.y;
        const z = Math.floor(yz / dims.y);
        for (const [dx, dy, dz] of neighborOffsets) {
            const nx = x + dx;
            const ny = y + dy;
            const nz = z + dz;
            if (nx < 0 || ny < 0 || nz < 0 || nx >= dims.x || ny >= dims.y || nz >= dims.z) {
                continue;
            }
            const nIdx = indexFromCoords(nx, ny, nz, dims);
            if (exterior[nIdx] !== 0) {
                continue;
            }
            if (voxels[nIdx] !== 0) {
                continue;
            }
            voxels[nIdx] = value;
            fillQueue.push(nIdx);
        }
    }
};
const convertSinglePrimitive = (primitive, textureLookup, patternLookup, materialLookup, expansion) => {
    const targetSize = computeTargetSize(primitive.size, expansion);
    const dims = axisDimsFromSize(targetSize);
    const volume = createVoxelVolume(dims);
    const offsets = expansion > 0 ? { x: expansion / 2, y: expansion / 2, z: expansion / 2 } : { x: 0, y: 0, z: 0 };
    paintPrimitiveIntoVolume(primitive, textureLookup, patternLookup, materialLookup, dims, offsets, volume);
    fillInteriorVoxels(volume);
    return volume;
};
function convertTexturedBoxesToVoxel(options) {
    const { base, overlay, textureLookup, patternLookup, materialLookup } = options;
    const targetId = options.targetId ?? base.id;
    const mode = options.mode ?? 'outer_wrap';
    if (!overlay) {
        const volume = convertSinglePrimitive(base, textureLookup, patternLookup, materialLookup, 0);
        fillInteriorVoxels(volume);
        return {
            id: targetId,
            kind: 'voxel_box',
            size: [volume.dims.x, volume.dims.y, volume.dims.z],
            voxels: volume.voxels,
            palette: volume.palette,
        };
    }
    switch (mode) {
        case 'overlay_overwrite': {
            const dims = axisDimsFromSize(base.size);
            const volume = createVoxelVolume(dims);
            paintPrimitiveIntoVolume(base, textureLookup, patternLookup, materialLookup, dims, { x: 0, y: 0, z: 0 }, volume);
            paintPrimitiveIntoVolume(overlay, textureLookup, patternLookup, materialLookup, dims, { x: 0, y: 0, z: 0 }, volume);
            fillInteriorVoxels(volume);
            return {
                id: targetId,
                kind: 'voxel_box',
                size: [volume.dims.x, volume.dims.y, volume.dims.z],
                voxels: volume.voxels,
                palette: volume.palette,
            };
        }
        case 'base_inset': {
            const overlayDims = axisDimsFromSize(overlay.size);
            const volume = createVoxelVolume(overlayDims);
            paintPrimitiveIntoVolume(overlay, textureLookup, patternLookup, materialLookup, overlayDims, { x: 0, y: 0, z: 0 }, volume);
            const insetDims = {
                x: Math.max(1, overlayDims.x - 2),
                y: Math.max(1, overlayDims.y - 2),
                z: Math.max(1, overlayDims.z - 2),
            };
            const insetOffsets = { x: 1, y: 1, z: 1 };
            paintPrimitiveIntoVolume(base, textureLookup, patternLookup, materialLookup, insetDims, { x: 0, y: 0, z: 0 }, volume, clampOffsetsToDims(insetOffsets, overlayDims), { u: 1, v: 1 });
            fillInteriorVoxels(volume);
            return {
                id: targetId,
                kind: 'voxel_box',
                size: [volume.dims.x, volume.dims.y, volume.dims.z],
                voxels: volume.voxels,
                palette: volume.palette,
            };
        }
        case 'outer_wrap':
        default: {
            const innerSize = [
                round(toNumber(base.size?.[0])),
                round(toNumber(base.size?.[1])),
                round(toNumber(base.size?.[2])),
            ];
            const outerSize = computeTargetSize(overlay.size, 2);
            const innerDims = axisDimsFromSize(innerSize);
            const outerDims = axisDimsFromSize(outerSize);
            const volume = createVoxelVolume(outerDims);
            paintPrimitiveIntoVolume(overlay, textureLookup, patternLookup, materialLookup, outerDims, { x: 1, y: 1, z: 1 }, volume);
            const innerTranslation = {
                x: Math.floor((outerDims.x - innerDims.x) / 2),
                y: Math.floor((outerDims.y - innerDims.y) / 2),
                z: Math.floor((outerDims.z - innerDims.z) / 2),
            };
            paintPrimitiveIntoVolume(base, textureLookup, patternLookup, materialLookup, innerDims, { x: 0, y: 0, z: 0 }, volume, clampOffsetsToDims(innerTranslation, outerDims));
            fillInteriorVoxels(volume);
            return {
                id: targetId,
                kind: 'voxel_box',
                size: [volume.dims.x, volume.dims.y, volume.dims.z],
                voxels: volume.voxels,
                palette: volume.palette,
            };
        }
    }
}
//# sourceMappingURL=textured-to-voxel.js.map