"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BoxelValidationError = void 0;
exports.isTupleOfNumbers = isTupleOfNumbers;
exports.assertVec3 = assertVec3;
exports.assertVec2 = assertVec2;
exports.assertPattern = assertPattern;
exports.assertMaterial = assertMaterial;
exports.assertTexture = assertTexture;
exports.assertPrimitive = assertPrimitive;
exports.assertAnimation = assertAnimation;
exports.assertTexturedBoxFaceSizes = assertTexturedBoxFaceSizes;
exports.validateBoxel = validateBoxel;
exports.assertBoxel = assertBoxel;
exports.collectBoxelValidationIssues = collectBoxelValidationIssues;
const textures_1 = require("./textures");
const VEC2_LENGTH = 2;
const VEC3_LENGTH = 3;
const CONNECT_MODES = ['none', 'child', 'parent', 'both'];
const isConnectMode = (value) => typeof value === 'string' && CONNECT_MODES.includes(value);
class BoxelValidationError extends Error {
    constructor(message, issues) {
        super(message);
        this.issues = issues;
        this.name = 'BoxelValidationError';
    }
}
exports.BoxelValidationError = BoxelValidationError;
const sanitizeMessage = (error) => {
    if (!error)
        return 'Unknown validation error';
    if (typeof error === 'string')
        return error;
    if (error instanceof Error)
        return error.message;
    if (typeof error === 'object' && 'message' in error) {
        const candidate = error.message;
        return typeof candidate === 'string' ? candidate : 'Unknown validation error';
    }
    return String(error);
};
const pushIssue = (issues, path, error) => {
    issues.push({ path, message: sanitizeMessage(error) });
};
function isTupleOfNumbers(value, expectedLength) {
    return Array.isArray(value) && value.length === expectedLength && value.every(item => typeof item === 'number' && Number.isFinite(item));
}
function assertVec3(value, context) {
    if (!isTupleOfNumbers(value, VEC3_LENGTH)) {
        throw new Error(`Invalid Vec3 for ${context}`);
    }
}
function assertVec2(value, context) {
    if (!isTupleOfNumbers(value, VEC2_LENGTH)) {
        throw new Error(`Invalid Vec2 for ${context}`);
    }
}
const readVec3 = (value, context, issues, path) => {
    try {
        assertVec3(value, context);
        return value;
    }
    catch (error) {
        pushIssue(issues, path, error);
        return null;
    }
};
const readVec2 = (value, context, issues, path) => {
    try {
        assertVec2(value, context);
        return value;
    }
    catch (error) {
        pushIssue(issues, path, error);
        return null;
    }
};
function assertPattern(value, index) {
    if (!value || typeof value !== 'object') {
        throw new Error(`Pattern at index ${index} must be an object`);
    }
    const pattern = value;
    if (typeof pattern.id !== 'string') {
        throw new Error(`Pattern at index ${index} is missing id`);
    }
    assertVec2(pattern.size, `pattern ${pattern.id} size`);
    const patternSize = pattern.size;
    if (!Array.isArray(pattern.data) || !pattern.data.every(cell => Number.isInteger(cell) && cell >= 0)) {
        throw new Error(`Pattern ${pattern.id} data must be an array of non-negative integers`);
    }
    const expectedLength = patternSize[0] * patternSize[1];
    if (pattern.data.length !== expectedLength) {
        throw new Error(`Pattern ${pattern.id} data length ${pattern.data.length} does not match expected size ${expectedLength}`);
    }
}
function assertMaterial(value, index) {
    if (!value || typeof value !== 'object') {
        throw new Error(`Material at index ${index} must be an object`);
    }
    const material = value;
    if (typeof material.id !== 'string') {
        throw new Error(`Material at index ${index} is missing id`);
    }
    assertVec3(material.baseColor, `material ${material.id} baseColor`);
    if (material.emissiveColor !== undefined) {
        assertVec3(material.emissiveColor, `material ${material.id} emissiveColor`);
    }
    if (material.opacity !== undefined && (typeof material.opacity !== 'number' || !Number.isFinite(material.opacity))) {
        throw new Error(`Material ${material.id} opacity must be a finite number`);
    }
}
function assertTexture(value, index) {
    if (!value || typeof value !== 'object') {
        throw new Error(`Texture at index ${index} must be an object`);
    }
    const texture = value;
    if (typeof texture.id !== 'string') {
        throw new Error(`Texture at index ${index} is missing id`);
    }
    if (typeof texture.pattern !== 'string') {
        throw new Error(`Texture ${texture.id} pattern must be a string`);
    }
    if (!Array.isArray(texture.materials) || !texture.materials.every(id => typeof id === 'string')) {
        throw new Error(`Texture ${texture.id} materials must be an array of strings`);
    }
}
function assertPlainBox(primitive, context) {
    if (primitive.kind !== 'plain_box') {
        throw new Error(`${context} expected kind plain_box`);
    }
    if (typeof primitive.material !== 'string') {
        throw new Error(`${context} material must be a string`);
    }
}
function assertTexturedBox(primitive, context) {
    if (primitive.kind !== 'textured_box') {
        throw new Error(`${context} expected kind textured_box`);
    }
    const candidate = primitive;
    const faces = ['top', 'bottom', 'north', 'south', 'east', 'west', 'sides', 'all'];
    faces.forEach(face => {
        const value = candidate[face];
        if (value !== undefined && typeof value !== 'string') {
            throw new Error(`${context} face ${face} must be a string id`);
        }
    });
}
function assertVoxelBox(primitive, context) {
    if (primitive.kind !== 'voxel_box') {
        throw new Error(`${context} expected kind voxel_box`);
    }
    const candidate = primitive;
    if (!Array.isArray(candidate.voxels) || !candidate.voxels.every(cell => typeof cell === 'number')) {
        throw new Error(`${context} voxels must be an array of numbers`);
    }
    if (candidate.palette !== undefined) {
        if (!Array.isArray(candidate.palette) || !candidate.palette.every(entry => typeof entry === 'string')) {
            throw new Error(`${context} palette must be an array of material ids`);
        }
    }
    const size = primitive.size;
    assertVec3(size, `${context} size`);
    const dims = size.map(axis => Math.round(axis));
    if (dims.some(value => value <= 0 || !Number.isFinite(value))) {
        throw new Error(`${context} size must contain positive finite numbers`);
    }
    const expected = dims[0] * dims[1] * dims[2];
    if (candidate.voxels.length !== expected) {
        throw new Error(`${context} voxels length ${candidate.voxels.length} does not match size product ${expected}`);
    }
}
function assertComboBox(primitive, context) {
    if (primitive.kind !== 'combo_box') {
        throw new Error(`${context} expected kind combo_box`);
    }
    const candidate = primitive;
    if (!Array.isArray(candidate.slots)) {
        throw new Error(`${context} slots must be an array`);
    }
    candidate.slots.forEach((slot, idx) => {
        if (!slot || typeof slot !== 'object') {
            throw new Error(`${context} slot ${idx} must be an object`);
        }
        const slotId = slot.id;
        if (slotId !== undefined && typeof slotId !== 'string') {
            throw new Error(`${context} slot ${idx} id must be a string when provided`);
        }
        if (typeof slot.primitive !== 'string') {
            throw new Error(`${context} slot ${idx} primitive must be a string`);
        }
        assertVec3(slot.offset, `${context} slot ${idx} offset`);
    });
}
function assertPrimitive(value, index) {
    if (!value || typeof value !== 'object') {
        throw new Error(`Primitive at index ${index} must be an object`);
    }
    const primitive = value;
    if (typeof primitive.id !== 'string') {
        throw new Error(`Primitive at index ${index} is missing id`);
    }
    const primitiveId = primitive.id;
    if (typeof primitive.kind !== 'string') {
        throw new Error(`Primitive ${primitiveId} kind must be a string discriminator`);
    }
    assertVec3(primitive.size, `primitive ${primitiveId} size`);
    switch (primitive.kind) {
        case 'plain_box':
            assertPlainBox(primitive, `primitive ${primitiveId}`);
            break;
        case 'textured_box':
            assertTexturedBox(primitive, `primitive ${primitiveId}`);
            break;
        case 'voxel_box':
            assertVoxelBox(primitive, `primitive ${primitiveId}`);
            break;
        case 'combo_box':
            assertComboBox(primitive, `primitive ${primitiveId}`);
            break;
        default: {
            const unknownKind = value.kind;
            throw new Error(`Unknown primitive kind ${String(unknownKind)} for primitive ${primitiveId}`);
        }
    }
}
const animationProperties = ['position', 'rotation', 'scale'];
function assertAnimationChannel(value, context) {
    if (!value || typeof value !== 'object') {
        throw new Error(`${context} channel must be an object`);
    }
    const channel = value;
    if (typeof channel.node !== 'string') {
        throw new Error(`${context} channel node must be a string`);
    }
    if (!animationProperties.includes(channel.property)) {
        throw new Error(`${context} channel property must be one of ${animationProperties.join(', ')}`);
    }
    if (!Array.isArray(channel.keyframes)) {
        throw new Error(`${context} keyframes must be an array`);
    }
    channel.keyframes.forEach((keyframe, idx) => {
        if (!keyframe || typeof keyframe !== 'object') {
            throw new Error(`${context} keyframe ${idx} must be an object`);
        }
        const reference = keyframe;
        if (typeof reference.time !== 'number' || !Number.isFinite(reference.time) || reference.time < 0) {
            throw new Error(`${context} keyframe ${idx} time must be a non-negative number`);
        }
        assertVec3(reference.value, `${context} keyframe ${idx} value`);
    });
}
function assertAnimation(value, index) {
    if (!value || typeof value !== 'object') {
        throw new Error(`Animation at index ${index} must be an object`);
    }
    const animation = value;
    if (typeof animation.id !== 'string' || !animation.id) {
        throw new Error(`Animation at index ${index} is missing id`);
    }
    if (typeof animation.duration !== 'number' || !Number.isFinite(animation.duration) || animation.duration < 0) {
        throw new Error(`Animation ${animation.id} duration must be a non-negative finite number`);
    }
    if (!Array.isArray(animation.channels)) {
        throw new Error(`Animation ${animation.id} channels must be an array`);
    }
    animation.channels.forEach((channel, channelIndex) => assertAnimationChannel(channel, `animation ${animation.id} channel ${channelIndex}`));
}
const FACE_DIMENSIONS = {
    top: size => [size[0], size[2]],
    bottom: size => [size[0], size[2]],
    north: size => [size[0], size[1]],
    south: size => [size[0], size[1]],
    east: size => [size[2], size[1]],
    west: size => [size[2], size[1]],
    sides: () => [0, 0],
    all: () => [0, 0],
};
function normalizeDimension(value) {
    if (!Number.isFinite(value)) {
        return NaN;
    }
    return Math.abs(Math.round(value));
}
function assertTexturedBoxFaceSizes(box, textureLookup, patternLookup, materialLookup) {
    const size = box.size;
    const [sizeX, sizeY, sizeZ] = size;
    [sizeX, sizeY, sizeZ].forEach((component, idx) => {
        if (!Number.isFinite(component)) {
            throw new Error(`primitive ${box.id} size component ${idx} must be finite to validate textures`);
        }
    });
    const normalizedSize = [
        normalizeDimension(sizeX),
        normalizeDimension(sizeY),
        normalizeDimension(sizeZ),
    ];
    if (normalizedSize.some(dimension => dimension === 0)) {
        throw new Error(`primitive ${box.id} must not contain zero-sized dimensions for textured faces`);
    }
    ['top', 'bottom', 'north', 'south', 'east', 'west'].forEach(face => {
        const textureId = (0, textures_1.resolveFaceTextureId)(box, face);
        if (!textureId) {
            return;
        }
        const texture = textureLookup.get(textureId);
        if (!texture) {
            throw new Error(`primitive ${box.id} face ${face} references missing texture ${textureId}`);
        }
        const pattern = patternLookup.get(texture.pattern);
        if (!pattern) {
            throw new Error(`texture ${texture.id} referenced by primitive ${box.id} face ${face} is missing pattern ${texture.pattern}`);
        }
        if (!Array.isArray(pattern.size) || pattern.size.length !== 2) {
            throw new Error(`pattern ${pattern.id} must define a 2D size for primitive ${box.id} face ${face}`);
        }
        const size2D = pattern.size;
        const patternWidth = normalizeDimension(size2D[0]);
        const patternHeight = normalizeDimension(size2D[1]);
        if (!Number.isFinite(patternWidth) || !Number.isFinite(patternHeight) || patternWidth <= 0 || patternHeight <= 0) {
            throw new Error(`pattern ${pattern.id} has invalid size for primitive ${box.id} face ${face}`);
        }
        const [expectedWidthRaw, expectedHeightRaw] = FACE_DIMENSIONS[face](normalizedSize);
        const expectedWidth = normalizeDimension(expectedWidthRaw);
        const expectedHeight = normalizeDimension(expectedHeightRaw);
        if (patternWidth !== expectedWidth || patternHeight !== expectedHeight) {
            throw new Error(`primitive ${box.id} face ${face} expects texture size ${expectedWidth}x${expectedHeight} but texture ${textureId} pattern ${pattern.id} is ${patternWidth}x${patternHeight}`);
        }
    });
    ['top', 'bottom', 'north', 'south', 'east', 'west'].forEach(face => {
        const textureId = (0, textures_1.resolveOverlayFaceTextureId)(box, face);
        if (!textureId) {
            return;
        }
        const texture = textureLookup.get(textureId);
        if (!texture) {
            throw new Error(`primitive ${box.id} overlay face ${face} references missing texture ${textureId}`);
        }
        const pattern = patternLookup.get(texture.pattern);
        if (!pattern) {
            throw new Error(`texture ${texture.id} referenced by primitive ${box.id} overlay face ${face} is missing pattern ${texture.pattern}`);
        }
        if (!Array.isArray(pattern.size) || pattern.size.length !== 2) {
            throw new Error(`pattern ${pattern.id} must define a 2D size for primitive ${box.id} overlay face ${face}`);
        }
        const size2D = pattern.size;
        const patternWidth = normalizeDimension(size2D[0]);
        const patternHeight = normalizeDimension(size2D[1]);
        if (!Number.isFinite(patternWidth) || !Number.isFinite(patternHeight) || patternWidth <= 0 || patternHeight <= 0) {
            throw new Error(`pattern ${pattern.id} has invalid size for primitive ${box.id} overlay face ${face}`);
        }
        const [expectedWidthRaw, expectedHeightRaw] = FACE_DIMENSIONS[face](normalizedSize);
        const expectedWidth = normalizeDimension(expectedWidthRaw);
        const expectedHeight = normalizeDimension(expectedHeightRaw);
        if (patternWidth !== expectedWidth || patternHeight !== expectedHeight) {
            throw new Error(`primitive ${box.id} overlay face ${face} expects texture size ${expectedWidth}x${expectedHeight} but texture ${textureId} pattern ${pattern.id} is ${patternWidth}x${patternHeight}`);
        }
    });
    const mode = box.renderMode ?? 'plain';
    if (mode !== 'plain' && mode !== 'cutout') {
        throw new Error(`primitive ${box.id} has invalid renderMode ${String(mode)}`);
    }
    const faceTextureIds = (0, textures_1.collectFaceTextureIds)(box);
    const usesTransparency = faceTextureIds.some(textureId => {
        const texture = textureLookup.get(textureId);
        if (!texture) {
            return false;
        }
        return texture.materials.some(materialId => {
            const material = materialLookup.get(materialId);
            if (!material) {
                return false;
            }
            if (material.opacity === undefined) {
                return false;
            }
            return Number(material.opacity) < 1;
        });
    });
    if (mode === 'plain' && usesTransparency) {
        throw new Error(`primitive ${box.id} renderMode plain but references textures with transparent materials`);
    }
}
function assertNode(value, path, primitiveLookup) {
    if (!value || typeof value !== 'object') {
        throw new Error(`Node ${path} must be an object`);
    }
    const node = value;
    if (typeof node.id !== 'string') {
        throw new Error(`Node ${path} is missing id`);
    }
    assertVec3(node.position, `node ${node.id} position`);
    if (node.rotation !== undefined) {
        assertVec3(node.rotation, `node ${node.id} rotation`);
    }
    if (node.scale !== undefined) {
        assertVec3(node.scale, `node ${node.id} scale`);
    }
    if (node.size !== undefined) {
        assertVec3(node.size, `node ${node.id} size`);
    }
    if (node.center !== undefined) {
        assertVec3(node.center, `node ${node.id} center`);
    }
    if (node.connect !== undefined) {
        const connectValue = node.connect;
        if (connectValue === true) {
            node.connect = 'child';
        }
        else if (connectValue === false) {
            delete node.connect;
        }
        else if (typeof connectValue === 'string') {
            if (!isConnectMode(connectValue)) {
                throw new Error(`Node ${node.id} connect must be one of ${CONNECT_MODES.join(', ')}`);
            }
            node.connect = connectValue;
        }
        else {
            throw new Error(`Node ${node.id} connect must be a boolean or enum value`);
        }
    }
    if (node.mesh !== undefined) {
        if (typeof node.mesh !== 'string') {
            throw new Error(`Node ${node.id} mesh must be a primitive id`);
        }
        if (!primitiveLookup.has(node.mesh)) {
            throw new Error(`Node ${node.id} references unknown primitive ${node.mesh}`);
        }
    }
    if (!Array.isArray(node.children)) {
        throw new Error(`Node ${node.id} children must be an array`);
    }
    node.children.forEach((child, idx) => assertNode(child, `${path}.${child?.id ?? idx}`, primitiveLookup));
}
function collectNodeIds(node, set) {
    set.add(node.id);
    node.children.forEach(child => collectNodeIds(child, set));
}
function assertAnimationChannels(animation, nodeIds) {
    const seenChannels = new Set();
    const epsilon = 1e-5;
    animation.channels.forEach((channel, channelIndex) => {
        const key = `${channel.node}:${channel.property}`;
        if (seenChannels.has(key)) {
            throw new Error(`Animation ${animation.id} channel ${channelIndex} duplicates node ${channel.node} property ${channel.property}`);
        }
        seenChannels.add(key);
        if (!nodeIds.has(channel.node)) {
            throw new Error(`Animation ${animation.id} channel ${channelIndex} references unknown node ${channel.node}`);
        }
        let previousTime = -Infinity;
        channel.keyframes.forEach((keyframe, keyframeIndex) => {
            if (keyframe.time > animation.duration + epsilon) {
                throw new Error(`Animation ${animation.id} channel ${channelIndex} keyframe ${keyframeIndex} time ${keyframe.time} exceeds duration ${animation.duration}`);
            }
            if (keyframe.time + epsilon < previousTime) {
                throw new Error(`Animation ${animation.id} channel ${channelIndex} keyframes must be in ascending time order (found ${keyframe.time} after ${previousTime})`);
            }
            previousTime = keyframe.time;
        });
    });
}
function validateBoxel(candidate) {
    const issues = [];
    if (!candidate || typeof candidate !== 'object') {
        pushIssue(issues, 'boxel', 'BoxelDefinition must be an object');
        return { boxel: null, issues };
    }
    const raw = candidate;
    let id = null;
    if (typeof raw.id === 'string' && raw.id.trim().length > 0) {
        id = raw.id;
    }
    else {
        pushIssue(issues, 'boxel.id', 'BoxelDefinition id must be a non-empty string');
    }
    const primitives = [];
    const primitiveLookup = new Map();
    if (!Array.isArray(raw.primitives)) {
        pushIssue(issues, 'boxel.primitives', 'BoxelDefinition primitives must be an array');
    }
    else {
        raw.primitives.forEach((primitive, index) => {
            try {
                assertPrimitive(primitive, index);
                primitives.push(primitive);
                primitiveLookup.set(primitive.id, primitive);
            }
            catch (error) {
                pushIssue(issues, `boxel.primitives[${index}]`, error);
            }
        });
    }
    let geometry = null;
    const geometryNodeIds = new Set();
    if (!raw.geometry || typeof raw.geometry !== 'object') {
        pushIssue(issues, 'boxel.geometry', 'BoxelDefinition geometry is missing or invalid');
    }
    else {
        const geo = raw.geometry;
        if (typeof geo.id !== 'string') {
            pushIssue(issues, 'boxel.geometry.id', 'Geometry id must be a string');
        }
        else if (!geo.root) {
            pushIssue(issues, 'boxel.geometry.root', 'Geometry root is missing');
        }
        else {
            try {
                assertNode(geo.root, 'root', primitiveLookup);
                geometry = geo;
                collectNodeIds(geo.root, geometryNodeIds);
            }
            catch (error) {
                pushIssue(issues, 'boxel.geometry.root', error);
            }
        }
    }
    const patterns = [];
    const patternLookup = new Map();
    if (!Array.isArray(raw.patterns)) {
        pushIssue(issues, 'boxel.patterns', 'BoxelDefinition patterns must be an array');
    }
    else {
        raw.patterns.forEach((pattern, index) => {
            try {
                assertPattern(pattern, index);
                patterns.push(pattern);
                patternLookup.set(pattern.id, pattern);
            }
            catch (error) {
                pushIssue(issues, `boxel.patterns[${index}]`, error);
            }
        });
    }
    const materials = [];
    const materialLookup = new Map();
    if (!Array.isArray(raw.materials)) {
        pushIssue(issues, 'boxel.materials', 'BoxelDefinition materials must be an array');
    }
    else {
        raw.materials.forEach((material, index) => {
            try {
                assertMaterial(material, index);
                materials.push(material);
                materialLookup.set(material.id, material);
            }
            catch (error) {
                pushIssue(issues, `boxel.materials[${index}]`, error);
            }
        });
    }
    const textures = [];
    const textureLookup = new Map();
    if (!Array.isArray(raw.textures)) {
        pushIssue(issues, 'boxel.textures', 'BoxelDefinition textures must be an array');
    }
    else {
        raw.textures.forEach((texture, index) => {
            try {
                assertTexture(texture, index);
                const typed = texture;
                const pattern = patternLookup.get(typed.pattern);
                if (!pattern) {
                    throw new Error(`Texture ${typed.id} references unknown pattern ${typed.pattern}`);
                }
                typed.materials.forEach(materialId => {
                    if (!materialLookup.has(materialId)) {
                        throw new Error(`Texture ${typed.id} references unknown material ${materialId}`);
                    }
                });
                const paletteSize = typed.materials.length;
                for (let i = 0; i < pattern.data.length; i += 1) {
                    const value = pattern.data[i];
                    if (value >= paletteSize) {
                        throw new Error(`Texture ${typed.id} pattern index ${value} exceeds palette size ${paletteSize} (pattern ${pattern.id})`);
                    }
                }
                textures.push(typed);
                textureLookup.set(typed.id, typed);
            }
            catch (error) {
                pushIssue(issues, `boxel.textures[${index}]`, error);
            }
        });
    }
    if (textures.length > 0) {
        primitives
            .filter(primitive => primitive.kind === 'textured_box')
            .forEach(primitive => {
            try {
                assertTexturedBoxFaceSizes(primitive, textureLookup, patternLookup, materialLookup);
            }
            catch (error) {
                pushIssue(issues, `boxel.primitives[${primitive.id}]`, error);
            }
        });
    }
    const animations = [];
    if (!Array.isArray(raw.animations)) {
        if (raw.animations !== undefined) {
            pushIssue(issues, 'boxel.animations', 'BoxelDefinition animations must be an array');
        }
    }
    else {
        raw.animations.forEach((animation, index) => {
            try {
                assertAnimation(animation, index);
                if (geometry) {
                    assertAnimationChannels(animation, geometryNodeIds);
                }
                animations.push(animation);
            }
            catch (error) {
                pushIssue(issues, `boxel.animations[${index}]`, error);
            }
        });
    }
    if (id === null || !geometry) {
        return { boxel: null, issues };
    }
    const boxel = {
        id,
        geometry,
        patterns,
        materials,
        textures,
        primitives,
        animations,
    };
    return { boxel: issues.length === 0 ? boxel : null, issues };
}
function assertBoxel(candidate) {
    const { boxel, issues } = validateBoxel(candidate);
    if (boxel) {
        return boxel;
    }
    throw new BoxelValidationError('BoxelDefinition validation failed', issues);
}
function collectBoxelValidationIssues(candidate) {
    return validateBoxel(candidate).issues;
}
//# sourceMappingURL=validation.js.map