"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseBoxelJson = exports.parseBoxel = exports.formatBoxel = void 0;
const format_utils_1 = require("./format-utils");
const validation_1 = require("./validation");
const INDENT = '  ';
const indent = (level) => INDENT.repeat(level);
const q = (value) => JSON.stringify(value);
const formatVector = (vec) => `[${vec.map(format_utils_1.formatNumber).join(', ')}]`;
const isZeroVector = (vec) => Array.isArray(vec) && vec.length > 0 && vec.every(value => Math.abs(value) < 1e-6);
const isUnitVector = (vec) => Array.isArray(vec) && vec.length === 3 && vec.every(value => Math.abs(value - 1) < 1e-6);
const formatStringArray = (values, level) => {
    if (values.length === 0)
        return '[]';
    return `[\n${values
        .map(value => `${indent(level + 1)}${q(value)}`)
        .join(',\n')}\n${indent(level)}]`;
};
const formatPatternData = (pattern, level) => {
    const width = Math.max(1, Math.round(pattern.size[0] ?? 1));
    const height = Math.max(1, Math.round(pattern.size[1] ?? 1));
    if (!Array.isArray(pattern.data) || pattern.data.length === 0 || width <= 0) {
        return [`${indent(level)}[]`];
    }
    const lines = [`${indent(level)}[`];
    for (let y = 0; y < height; y += 1) {
        const start = y * width;
        const slice = pattern.data.slice(start, start + width);
        const suffix = y === height - 1 ? '' : ',';
        lines.push(`${indent(level + 1)}${slice.map(format_utils_1.formatNumber).join(', ')}${suffix}`);
    }
    lines.push(`${indent(level)}]`);
    return lines;
};
const formatVoxelData = (voxel, level) => {
    const [sx, sy, sz] = voxel.size ?? [1, 1, 1];
    if (!Array.isArray(voxel.voxels) || voxel.voxels.length === 0 || sx <= 0 || sy <= 0 || sz <= 0) {
        return [`${indent(level)}[]`];
    }
    const lines = [`${indent(level)}[`];
    for (let z = 0; z < sz; z += 1) {
        for (let y = 0; y < sy; y += 1) {
            const row = [];
            for (let x = 0; x < sx; x += 1) {
                const index = x + y * sx + z * sx * sy;
                row.push(voxel.voxels[index] ?? 0);
            }
            const isLastRow = z === sz - 1 && y === sy - 1;
            const suffix = isLastRow ? '' : ',';
            lines.push(`${indent(level + 1)}${row.map(format_utils_1.formatNumber).join(', ')}${suffix}`);
        }
    }
    lines.push(`${indent(level)}]`);
    return lines;
};
const pushEntries = (lines, entries, level) => {
    entries.forEach((entry, entryIndex) => {
        const suffix = entryIndex === entries.length - 1 ? '' : ',';
        if (Array.isArray(entry)) {
            entry.forEach((line, lineIndex) => {
                const trailing = lineIndex === entry.length - 1 ? suffix : '';
                lines.push(line + trailing);
            });
        }
        else {
            lines.push(`${indent(level)}${entry}${suffix}`);
        }
    });
};
const formatNode = (node, level) => {
    const lines = [`${indent(level)}{`];
    const entries = [];
    entries.push(`"id": ${q(node.id)}`);
    entries.push(`"position": ${formatVector(node.position)}`);
    if (node.rotation && !isZeroVector(node.rotation)) {
        entries.push(`"rotation": ${formatVector(node.rotation)}`);
    }
    if (node.scale && !isUnitVector(node.scale)) {
        entries.push(`"scale": ${formatVector(node.scale)}`);
    }
    if (node.center && !isZeroVector(node.center)) {
        entries.push(`"center": ${formatVector(node.center)}`);
    }
    if (node.size)
        entries.push(`"size": ${formatVector(node.size)}`);
    if (node.connect !== undefined)
        entries.push(`"connect": ${q(node.connect)}`);
    if (node.mesh)
        entries.push(`"mesh": ${q(node.mesh)}`);
    if (node.children && node.children.length > 0) {
        const childrenLines = [`${indent(level + 1)}"children": [`];
        node.children.forEach((child, index) => {
            const childLines = formatNode(child, level + 2);
            childLines[childLines.length - 1] += index === node.children.length - 1 ? '' : ',';
            childrenLines.push(...childLines);
        });
        childrenLines.push(`${indent(level + 1)}]`);
        entries.push(childrenLines);
    }
    else {
        entries.push(`"children": []`);
    }
    pushEntries(lines, entries, level + 1);
    lines.push(`${indent(level)}}`);
    return lines;
};
const appendGeometry = (lines, geometry, level) => {
    lines.push(`${indent(level)}"geometry": {`);
    lines.push(`${indent(level + 1)}"id": ${q(geometry.id)},`);
    const nodeLines = formatNode(geometry.root, level + 1);
    lines.push(`${indent(level + 1)}"root": ${nodeLines[0].trimStart()}`);
    for (let i = 1; i < nodeLines.length; i += 1) {
        lines.push(nodeLines[i]);
    }
    lines.push(`${indent(level)}}`);
};
const appendPatterns = (lines, patterns, level) => {
    if (!patterns.length) {
        lines.push(`${indent(level)}"patterns": []`);
        return;
    }
    lines.push(`${indent(level)}"patterns": [`);
    patterns.forEach((pattern, index) => {
        lines.push(`${indent(level + 1)}{`);
        const entries = [];
        entries.push(`"id": ${q(pattern.id)}`);
        entries.push(`"size": ${formatVector(pattern.size)}`);
        const dataLines = formatPatternData(pattern, level + 2);
        dataLines[0] = `${indent(level + 2)}"data": ${dataLines[0].trimStart()}`;
        entries.push(dataLines);
        pushEntries(lines, entries, level + 2);
        lines.push(`${indent(level + 1)}}${index === patterns.length - 1 ? '' : ','}`);
    });
    lines.push(`${indent(level)}]`);
};
const appendMaterials = (lines, materials, level) => {
    if (!materials.length) {
        lines.push(`${indent(level)}"materials": []`);
        return;
    }
    lines.push(`${indent(level)}"materials": [`);
    materials.forEach((material, index) => {
        lines.push(`${indent(level + 1)}{`);
        const entries = [];
        entries.push(`"id": ${q(material.id)}`);
        entries.push(`"baseColor": ${formatVector(material.baseColor)}`);
        if (material.emissiveColor)
            entries.push(`"emissiveColor": ${formatVector(material.emissiveColor)}`);
        if (typeof material.opacity === 'number')
            entries.push(`"opacity": ${(0, format_utils_1.formatNumber)(material.opacity)}`);
        pushEntries(lines, entries, level + 2);
        lines.push(`${indent(level + 1)}}${index === materials.length - 1 ? '' : ','}`);
    });
    lines.push(`${indent(level)}]`);
};
const appendTextures = (lines, textures, level) => {
    if (!textures.length) {
        lines.push(`${indent(level)}"textures": []`);
        return;
    }
    lines.push(`${indent(level)}"textures": [`);
    textures.forEach((texture, index) => {
        lines.push(`${indent(level + 1)}{`);
        const entries = [];
        entries.push(`"id": ${q(texture.id)}`);
        entries.push(`"pattern": ${q(texture.pattern)}`);
        entries.push(texture.materials.length === 0
            ? `${indent(level + 2)}"materials": []`
            : `${indent(level + 2)}"materials": ${formatStringArray(texture.materials, level + 2)}`);
        pushEntries(lines, entries, level + 2);
        lines.push(`${indent(level + 1)}}${index === textures.length - 1 ? '' : ','}`);
    });
    lines.push(`${indent(level)}]`);
};
const formatTexturedFaces = (textured, level) => {
    const entries = [];
    if (textured.renderMode && textured.renderMode !== 'plain') {
        entries.push(`"renderMode": ${q(textured.renderMode)}`);
    }
    const fields = [
        'top',
        'bottom',
        'north',
        'south',
        'east',
        'west',
        'sides',
        'all',
        'topOverlay',
        'bottomOverlay',
        'northOverlay',
        'southOverlay',
        'eastOverlay',
        'westOverlay',
        'sidesOverlay',
        'allOverlay',
    ];
    fields
        .filter(face => typeof textured[face] === 'string')
        .forEach(face => {
        entries.push(`"${face}": ${q(textured[face])}`);
    });
    return entries;
};
const formatComboSlots = (combo, level) => {
    const lines = [];
    lines.push(`${indent(level)}"slots": [`);
    combo.slots.forEach((slot, index) => {
        lines.push(`${indent(level + 1)}{`);
        const entries = [];
        entries.push(`"id": ${q(slot.id)}`);
        entries.push(`"primitive": ${q(slot.primitive)}`);
        entries.push(`"offset": ${formatVector(slot.offset)}`);
        pushEntries(lines, entries, level + 2);
        lines.push(`${indent(level + 1)}}${index === combo.slots.length - 1 ? '' : ','}`);
    });
    lines.push(`${indent(level)}]`);
    return lines;
};
const appendPrimitives = (lines, primitives, level) => {
    if (!primitives.length) {
        lines.push(`${indent(level)}"primitives": []`);
        return;
    }
    lines.push(`${indent(level)}"primitives": [`);
    primitives.forEach((primitive, index) => {
        lines.push(`${indent(level + 1)}{`);
        const entries = [];
        entries.push(`"id": ${q(primitive.id)}`);
        entries.push(`"kind": ${q(primitive.kind)}`);
        entries.push(`"size": ${formatVector(primitive.size)}`);
        switch (primitive.kind) {
            case 'plain_box':
                entries.push(`"material": ${q(primitive.material)}`);
                break;
            case 'textured_box': {
                const texturedEntries = formatTexturedFaces(primitive, level + 2);
                entries.push(...texturedEntries);
                break;
            }
            case 'voxel_box': {
                const voxelLines = formatVoxelData(primitive, level + 2);
                entries.push(primitive.palette && primitive.palette.length > 0
                    ? `${indent(level + 2)}"palette": ${formatStringArray(primitive.palette, level + 2)}`
                    : `${indent(level + 2)}"palette": []`);
                const dataLines = [...voxelLines];
                dataLines[0] = `${indent(level + 2)}"voxels": ${dataLines[0].trimStart()}`;
                entries.push(dataLines);
                break;
            }
            case 'combo_box': {
                const slotLines = formatComboSlots(primitive, level + 2);
                entries.push(slotLines);
                break;
            }
            default:
                break;
        }
        pushEntries(lines, entries, level + 2);
        lines.push(`${indent(level + 1)}}${index === primitives.length - 1 ? '' : ','}`);
    });
    lines.push(`${indent(level)}]`);
};
const appendAnimations = (lines, animations, level) => {
    if (!animations.length) {
        lines.push(`${indent(level)}"animations": []`);
        return;
    }
    lines.push(`${indent(level)}"animations": [`);
    animations.forEach((animation, animationIndex) => {
        lines.push(`${indent(level + 1)}{`);
        const entries = [];
        entries.push(`"id": ${q(animation.id)}`);
        entries.push(`"duration": ${(0, format_utils_1.formatNumber)(animation.duration)}`);
        if (animation.channels.length === 0) {
            entries.push(`"channels": []`);
        }
        else {
            const channelLines = formatAnimationChannels(animation.channels, level + 2);
            entries.push(channelLines);
        }
        pushEntries(lines, entries, level + 2);
        lines.push(`${indent(level + 1)}}${animationIndex === animations.length - 1 ? '' : ','}`);
    });
    lines.push(`${indent(level)}]`);
};
const formatAnimationChannels = (channels, level) => {
    const lines = [];
    lines.push(`${indent(level)}"channels": [`);
    const orderedChannels = [...channels].sort((a, b) => {
        if (a.node === b.node) {
            return a.property.localeCompare(b.property);
        }
        return a.node.localeCompare(b.node);
    });
    orderedChannels.forEach((channel, channelIndex) => {
        lines.push(`${indent(level + 1)}{`);
        const entries = [];
        entries.push(`"node": ${q(channel.node)}`);
        entries.push(`"property": ${q(channel.property)}`);
        const keyframeLines = formatAnimationKeyframes(channel.keyframes, level + 2);
        entries.push(keyframeLines);
        pushEntries(lines, entries, level + 2);
        lines.push(`${indent(level + 1)}}${channelIndex === channels.length - 1 ? '' : ','}`);
    });
    lines.push(`${indent(level)}]`);
    return lines;
};
const formatAnimationKeyframes = (keyframes, level) => {
    if (!keyframes.length) {
        return [`${indent(level)}"keyframes": []`];
    }
    const lines = [];
    lines.push(`${indent(level)}"keyframes": [`);
    const orderedKeyframes = [...keyframes].sort((a, b) => a.time - b.time);
    orderedKeyframes.forEach((keyframe, keyframeIndex) => {
        lines.push(`${indent(level + 1)}{`);
        const entries = [];
        entries.push(`"time": ${(0, format_utils_1.formatNumber)(keyframe.time)}`);
        entries.push(`"value": ${formatVector(keyframe.value)}`);
        pushEntries(lines, entries, level + 2);
        lines.push(`${indent(level + 1)}}${keyframeIndex === keyframes.length - 1 ? '' : ','}`);
    });
    lines.push(`${indent(level)}]`);
    return lines;
};
const formatBoxel = (model) => {
    const lines = [];
    lines.push('{');
    lines.push(`${indent(1)}"id": ${q(model.id)},`);
    appendGeometry(lines, model.geometry, 1);
    const markComma = () => {
        const lastIndex = lines.length - 1;
        lines[lastIndex] = `${lines[lastIndex]},`;
    };
    markComma();
    appendPatterns(lines, model.patterns, 1);
    markComma();
    appendMaterials(lines, model.materials, 1);
    markComma();
    appendTextures(lines, model.textures, 1);
    markComma();
    appendPrimitives(lines, model.primitives, 1);
    markComma();
    appendAnimations(lines, model.animations, 1);
    lines.push('}');
    return lines.join('\n');
};
exports.formatBoxel = formatBoxel;
const sanitizeErrorMessage = (error) => {
    if (!error)
        return 'Unknown 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 error';
    }
    return String(error);
};
const parseBoxel = (candidate) => {
    return (0, validation_1.assertBoxel)(candidate);
};
exports.parseBoxel = parseBoxel;
const parseBoxelJson = (raw) => {
    let parsed;
    try {
        parsed = JSON.parse(raw);
    }
    catch (error) {
        const issue = {
            path: 'json',
            message: sanitizeErrorMessage(error),
        };
        throw new validation_1.BoxelValidationError('Failed to parse boxel JSON', [issue]);
    }
    return (0, exports.parseBoxel)(parsed);
};
exports.parseBoxelJson = parseBoxelJson;
//# sourceMappingURL=serialization.js.map