"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isGeneratedModel3DAssetTask = isGeneratedModel3DAssetTask;
exports.assertGeneratedModel3DAssetRoles = assertGeneratedModel3DAssetRoles;
exports.prepareModel3DAssetArtifacts = prepareModel3DAssetArtifacts;
const node_fs_1 = require("node:fs");
const promises_1 = require("node:fs/promises");
const node_path_1 = __importDefault(require("node:path"));
const boxel_core_1 = require("@playdrop/boxel-core");
const vox_three_1 = require("@playdrop/vox-three");
const types_1 = require("@playdrop/types");
const playwright_1 = require("../playwright");
const GENERATED_MODEL_3D_FORMATS = new Set(['PLAYDROP_BOXEL_JSON', 'VOX']);
const BOXEL_REQUIRED_FILE_ROLES = ['primary', 'mesh', 'preview'];
const VOX_REQUIRED_FILE_ROLES = ['primary', 'boxel', 'mesh', 'preview'];
const DEFAULT_PRESET = {
    meshMode: 'merged',
    previewMode: 'merged',
    previewDirection: 'preview',
    width: 1024,
    height: 1024,
};
const MODEL_PRESET_BY_SUBCATEGORY = {
    avatar: {
        meshMode: 'skinned',
        previewMode: 'skinned',
        previewDirection: 'headshot',
        width: 1024,
        height: 1024,
    },
    humanoid: {
        meshMode: 'skinned',
        previewMode: 'skinned',
        previewDirection: 'headshot',
        width: 1024,
        height: 1024,
    },
    creature: {
        meshMode: 'skinned',
        previewMode: 'skinned',
        previewDirection: 'headshot',
        width: 1024,
        height: 1024,
    },
    block: {
        meshMode: 'merged',
        previewMode: 'merged',
        previewDirection: 'preview',
        width: 1024,
        height: 1024,
    },
    generic: {
        meshMode: 'merged',
        previewMode: 'merged',
        previewDirection: 'preview',
        width: 1024,
        height: 1024,
    },
};
const TOOL_NAME = 'playdrop';
const WORKER_ENTRY_BASENAMES = ['model-worker.ts', 'model-worker.js'];
let workerBundlePromise = null;
function normalizeRole(role) {
    return role.trim().toLowerCase();
}
function resolveTaskLabel(task) {
    if ('kind' in task && task.kind === 'owned-asset') {
        return `${task.appName}:${task.name}`;
    }
    return task.name;
}
function getRolePath(task, role) {
    for (const [entryRole, filePath] of Object.entries(task.filePaths)) {
        if (normalizeRole(entryRole) === role && typeof filePath === 'string' && filePath.trim().length > 0) {
            return filePath;
        }
    }
    return null;
}
function inferGeneratedModel3DFormat(task, explicitFormat) {
    const normalizedFormat = typeof explicitFormat === 'string' ? explicitFormat.trim().toUpperCase() : '';
    if (GENERATED_MODEL_3D_FORMATS.has(normalizedFormat)) {
        return normalizedFormat;
    }
    const primaryPath = getRolePath(task, 'primary');
    if (!primaryPath) {
        return null;
    }
    const normalizedPath = primaryPath.trim().toLowerCase();
    if (normalizedPath.endsWith('.boxel.json')) {
        return 'PLAYDROP_BOXEL_JSON';
    }
    if (normalizedPath.endsWith('.vox')) {
        return 'VOX';
    }
    return null;
}
function getRequiredFileRoles(format) {
    return format === 'VOX' ? VOX_REQUIRED_FILE_ROLES : BOXEL_REQUIRED_FILE_ROLES;
}
function getMissingRequiredRoles(task, format) {
    const normalizedRoles = new Set(Object.keys(task.filePaths).map((role) => normalizeRole(role)));
    return getRequiredFileRoles(format).filter((role) => !normalizedRoles.has(role));
}
function resolveWorkerEntryPath() {
    for (const basename of WORKER_ENTRY_BASENAMES) {
        const candidate = node_path_1.default.join(__dirname, basename);
        if ((0, node_fs_1.existsSync)(candidate)) {
            return candidate;
        }
    }
    throw new Error(`[${TOOL_NAME}] Boxel worker entry file not found from ${__dirname}.`);
}
async function loadEsbuild() {
    const module = await Promise.resolve().then(() => __importStar(require('esbuild')));
    return module.build;
}
async function getWorkerBundle() {
    if (!workerBundlePromise) {
        workerBundlePromise = (async () => {
            const build = await loadEsbuild();
            const result = await build({
                entryPoints: [resolveWorkerEntryPath()],
                bundle: true,
                platform: 'browser',
                format: 'esm',
                target: 'es2022',
                write: false,
                sourcemap: false,
                logLevel: 'silent',
            });
            const output = result.outputFiles?.[0]?.text;
            if (!output) {
                throw new Error(`[${TOOL_NAME}] Failed to bundle boxel worker.`);
            }
            return output;
        })();
    }
    return workerBundlePromise;
}
async function withModelWorkerPage(callback, viewport) {
    return (0, playwright_1.withChromiumPage)(async ({ page }) => {
        const consoleErrors = [];
        page.on('console', (message) => {
            if (message.type() === 'error') {
                consoleErrors.push(message.text());
            }
        });
        page.on('pageerror', (error) => {
            consoleErrors.push(error?.message ?? String(error));
        });
        await page.setContent('<canvas id="viewport"></canvas>', { waitUntil: 'domcontentloaded' });
        const bundle = await getWorkerBundle();
        await page.addScriptTag({ type: 'module', content: bundle });
        await page.waitForFunction(() => typeof window.playdropBoxelWorker !== 'undefined');
        const result = await callback(page);
        if (consoleErrors.length > 0) {
            throw new Error(consoleErrors.join('\n'));
        }
        return result;
    }, { viewport });
}
async function exportBoxelWithWorker(boxel, mode) {
    try {
        const base64 = await withModelWorkerPage((page) => page.evaluate(({ model, exportMode }) => window.playdropBoxelWorker.exportBoxel(model, { mode: exportMode }), { model: boxel, exportMode: mode }), { width: 1024, height: 1024 });
        if (typeof base64 !== 'string' || base64.length === 0) {
            throw new Error('Model exporter did not return GLB data.');
        }
        return Buffer.from(base64, 'base64');
    }
    catch (error) {
        throw (0, playwright_1.createPlaywrightLaunchError)(TOOL_NAME, error);
    }
}
async function renderPreviewWithWorker(boxel, options) {
    try {
        const dataUrl = await withModelWorkerPage((page) => page.evaluate(({ model, renderOptions }) => window.playdropBoxelWorker.renderBoxel(model, renderOptions), {
            model: boxel,
            renderOptions: {
                mode: options.mode,
                direction: options.direction,
                width: options.width,
                height: options.height,
            },
        }), { width: options.width, height: options.height });
        if (typeof dataUrl !== 'string' || !dataUrl.startsWith('data:image/png;base64,')) {
            throw new Error('Model renderer did not return PNG data.');
        }
        return Buffer.from(dataUrl.slice('data:image/png;base64,'.length), 'base64');
    }
    catch (error) {
        throw (0, playwright_1.createPlaywrightLaunchError)(TOOL_NAME, error);
    }
}
async function renderVoxPreviewWithWorker(voxBuffer, options) {
    try {
        const dataUrl = await withModelWorkerPage((page) => page.evaluate(({ voxBase64, renderOptions }) => window.playdropBoxelWorker.renderVox(voxBase64, renderOptions), {
            voxBase64: voxBuffer.toString('base64'),
            renderOptions: {
                width: options.width,
                height: options.height,
            },
        }), { width: options.width, height: options.height });
        if (typeof dataUrl !== 'string' || !dataUrl.startsWith('data:image/png;base64,')) {
            throw new Error('VOX preview renderer did not return PNG data.');
        }
        return Buffer.from(dataUrl.slice('data:image/png;base64,'.length), 'base64');
    }
    catch (error) {
        throw (0, playwright_1.createPlaywrightLaunchError)(TOOL_NAME, error);
    }
}
function parseAndValidateBoxel(raw, label) {
    let parsed;
    try {
        parsed = (0, boxel_core_1.parseBoxel)(JSON.parse(raw));
    }
    catch (error) {
        const reason = error instanceof Error ? error.message : String(error);
        throw new Error(`Asset "${label}" has invalid files.primary Boxel JSON: ${reason}`);
    }
    const validation = (0, boxel_core_1.validateBoxel)(parsed);
    if (!validation.boxel || validation.issues.length > 0) {
        const summary = validation.issues.slice(0, 3).map((issue) => `${issue.path}: ${issue.message}`).join('; ');
        throw new Error(`Asset "${label}" files.primary failed boxel validation${summary ? `: ${summary}` : ''}`);
    }
    return validation.boxel;
}
function resolvePreset(subcategory) {
    const normalized = subcategory.trim().toLowerCase();
    return MODEL_PRESET_BY_SUBCATEGORY[normalized] ?? DEFAULT_PRESET;
}
function isGeneratedModel3DAssetTask(task, format) {
    const category = typeof task.category === 'string' ? task.category.trim().toUpperCase() : '';
    const subcategory = typeof task.subcategory === 'string' ? task.subcategory.trim() : '';
    if (category !== 'MODEL_3D' || !(0, types_1.isModel3DAssetSubcategory)('MODEL_3D', subcategory)) {
        return false;
    }
    return inferGeneratedModel3DFormat(task, format) !== null;
}
function assertGeneratedModel3DAssetRoles(task, format) {
    const generatedFormat = inferGeneratedModel3DFormat(task, format);
    if (!generatedFormat) {
        return;
    }
    const missing = getMissingRequiredRoles(task, generatedFormat);
    if (missing.length > 0) {
        const label = resolveTaskLabel(task);
        throw new Error(`Asset "${label}" requires files.${missing.join(', files.')} for format "${generatedFormat}".`);
    }
}
async function ensureExistingFile(filePath, label, role) {
    let fileStats;
    try {
        fileStats = await (0, promises_1.stat)(filePath);
    }
    catch (error) {
        const reason = error instanceof Error ? error.message : String(error);
        throw new Error(`Asset "${label}" files.${role} was not found at ${filePath}: ${reason}`);
    }
    if (!fileStats.isFile()) {
        throw new Error(`Asset "${label}" files.${role} path must be a file: ${filePath}`);
    }
}
async function loadBoxelFromPrimary(task, label) {
    const primaryPath = getRolePath(task, 'primary');
    if (!primaryPath) {
        throw new Error(`Asset "${label}" is missing files.primary.`);
    }
    await ensureExistingFile(primaryPath, label, 'primary');
    const raw = await (0, promises_1.readFile)(primaryPath, 'utf8');
    return parseAndValidateBoxel(raw, label);
}
async function loadValidatedVoxBuffer(task, label) {
    const primaryPath = getRolePath(task, 'primary');
    if (!primaryPath) {
        throw new Error(`Asset "${label}" is missing files.primary.`);
    }
    await ensureExistingFile(primaryPath, label, 'primary');
    const buffer = await (0, promises_1.readFile)(primaryPath);
    try {
        (0, vox_three_1.assertValidVoxBuffer)(buffer);
    }
    catch (error) {
        const reason = error instanceof Error ? error.message : String(error);
        throw new Error(`Asset "${label}" files.primary is not a valid .vox file: ${reason}`);
    }
    return buffer;
}
async function prepareModel3DAssetArtifacts(task, options = {}) {
    const generatedFormat = inferGeneratedModel3DFormat(task, task.format);
    if (!generatedFormat) {
        return false;
    }
    assertGeneratedModel3DAssetRoles(task, task.format);
    const label = resolveTaskLabel(task);
    const meshPath = getRolePath(task, 'mesh');
    const previewPath = getRolePath(task, 'preview');
    if (!meshPath || !previewPath) {
        throw new Error(`Asset "${label}" is missing required generated 3D file paths.`);
    }
    const voxBuffer = generatedFormat === 'VOX'
        ? await loadValidatedVoxBuffer(task, label)
        : null;
    const boxel = generatedFormat === 'VOX'
        ? await (0, vox_three_1.convertVoxToBoxel)(voxBuffer, task.name)
        : await loadBoxelFromPrimary(task, label);
    if (options.dryRun) {
        return true;
    }
    if (typeof task.subcategory !== 'string' || task.subcategory.trim().length === 0) {
        throw new Error(`Asset "${label}" is missing a model subcategory.`);
    }
    const preset = resolvePreset(task.subcategory);
    const [meshBuffer, previewBuffer] = await Promise.all([
        exportBoxelWithWorker(boxel, preset.meshMode),
        generatedFormat === 'VOX'
            ? renderVoxPreviewWithWorker(voxBuffer, {
                width: preset.width,
                height: preset.height,
            })
            : renderPreviewWithWorker(boxel, {
                mode: preset.previewMode,
                direction: preset.previewDirection,
                width: preset.width,
                height: preset.height,
            }),
    ]);
    await (0, promises_1.mkdir)(node_path_1.default.dirname(meshPath), { recursive: true });
    await (0, promises_1.mkdir)(node_path_1.default.dirname(previewPath), { recursive: true });
    await (0, promises_1.writeFile)(meshPath, meshBuffer);
    await (0, promises_1.writeFile)(previewPath, previewBuffer);
    if (generatedFormat === 'VOX') {
        const boxelPath = getRolePath(task, 'boxel');
        if (!boxelPath) {
            throw new Error(`Asset "${label}" is missing files.boxel.`);
        }
        await (0, promises_1.mkdir)(node_path_1.default.dirname(boxelPath), { recursive: true });
        await (0, promises_1.writeFile)(boxelPath, `${(0, boxel_core_1.formatBoxel)(boxel)}\n`, 'utf8');
    }
    return true;
}
