import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import crypto from 'crypto';
import { loadPlayCanvasProject } from '../loaders/playcanvas-loader.js';
import { loadPlayCraftProject } from '../loaders/playcraft-loader.js';
import { generateConfig } from '../generators/config-generator.js';
import { generateSettings } from '../generators/settings-generator.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
 * Vite 插件：从源代码构建 PlayCanvas 项目
 */
export function viteSourceBuilderPlugin(options) {
    let projectFormat = null;
    let projectConfig = null;
    let userScripts = [];
    let scriptPathMap = new Map(); // 模块路径 -> 文件系统路径
    let fileToModuleMap = new Map(); // 文件系统路径 -> 模块路径
    return {
        name: 'vite-plugin-source-builder',
        enforce: 'pre',
        async buildStart() {
            console.log('[SourceBuilder] 开始构建源代码项目...');
            // 1. 检测项目格式
            projectFormat = await detectFormat(options.projectDir);
            console.log(`[SourceBuilder] 检测到项目格式: ${projectFormat}`);
            // 2. 读取项目配置
            if (projectFormat === 'playcanvas') {
                projectConfig = await loadPlayCanvasProject(options.projectDir);
            }
            else {
                projectConfig = await loadPlayCraftProject(options.projectDir);
            }
            // 2.1 规范化资源文件 URL（补齐 file.url）
            await normalizeAssetUrls(projectConfig, options.projectDir);
            // 3. 收集用户脚本
            userScripts = await collectUserScripts(projectConfig, options.projectDir);
            console.log(`[SourceBuilder] 收集到 ${userScripts.length} 个用户脚本`);
            // 4. 构建脚本路径映射（基于文件夹层次）
            const mappings = buildScriptPathMapping(projectConfig, userScripts, options.projectDir);
            scriptPathMap = mappings.moduleToFile;
            fileToModuleMap = mappings.fileToModule;
            console.log(`[SourceBuilder] 构建了 ${scriptPathMap.size} 个路径映射`);
        },
        // 处理虚拟入口模块和自定义导入路径
        resolveId(id, importer) {
            if (id === 'virtual:game-scripts') {
                return '\0virtual:game-scripts';
            }
            // 解析自定义导入路径（如 Gameplay/GameplaySystem.mjs）
            if (scriptPathMap.has(id)) {
                return scriptPathMap.get(id);
            }
            // 通过 Import Map 解析裸模块说明符（如 gameRule、planck 等）
            // Import Map 中的路径是相对于 assets/ 目录的（Import Map 文件本身在 assets/Import Map.json 中）
            if (options.importMap?.content?.imports && !id.startsWith('./') && !id.startsWith('../') && !id.startsWith('/') && !id.startsWith('\0')) {
                const imports = options.importMap.content.imports;
                const assetsDir = path.join(options.projectDir, 'assets');
                // 精确匹配（如 "gameRule" -> "./Generated/GameRule/BreakshotPoolRule.mjs"）
                if (imports[id]) {
                    let resolved = imports[id];
                    // 去掉开头的 ./
                    if (resolved.startsWith('./')) {
                        resolved = resolved.slice(2);
                    }
                    const absPath = path.join(assetsDir, resolved);
                    console.log(`[SourceBuilder] Import Map 解析: ${id} -> ${absPath}`);
                    return absPath;
                }
                // 前缀匹配（如 "Gameplay/" -> "./LiteCreator/Gameplay/"）
                for (const [prefix, target] of Object.entries(imports)) {
                    if (prefix.endsWith('/') && id.startsWith(prefix)) {
                        const remainder = id.slice(prefix.length);
                        let targetBase = target;
                        if (targetBase.startsWith('./')) {
                            targetBase = targetBase.slice(2);
                        }
                        if (targetBase.endsWith('/')) {
                            targetBase = targetBase.slice(0, -1);
                        }
                        const absPath = path.join(assetsDir, targetBase, remainder);
                        console.log(`[SourceBuilder] Import Map 前缀解析: ${id} -> ${absPath}`);
                        return absPath;
                    }
                }
            }
            // 处理相对导入（../xxx 或 ./xxx）
            if (importer && (id.startsWith('./') || id.startsWith('../'))) {
                // 获取导入者的模块路径
                const importerModulePath = fileToModuleMap.get(importer);
                if (importerModulePath) {
                    // 基于模块路径解析相对导入
                    const importerDir = path.dirname(importerModulePath);
                    const resolvedModulePath = path.posix.join(importerDir, id);
                    // 从模块路径映射中查找实际文件
                    if (scriptPathMap.has(resolvedModulePath)) {
                        return scriptPathMap.get(resolvedModulePath);
                    }
                }
            }
            return null;
        },
        load(id) {
            if (id === '\0virtual:game-scripts') {
                // 生成入口文件，直接导入所有真实的脚本文件
                // 让 Vite 自然地处理这些文件及其依赖关系
                const imports = userScripts.map(script => {
                    const scriptPath = path.join(options.projectDir, script.path);
                    // 将 Windows 路径中的反斜杠转换为正斜杠，避免在 import 语句中被解释为转义字符
                    // 使用 split + join 方式更可靠地处理路径分隔符
                    const normalizedPath = scriptPath.split(path.sep).join('/');
                    console.log(`[SourceBuilder] 脚本路径: ${scriptPath} -> ${normalizedPath}`);
                    return `import '${normalizedPath}';`;
                }).join('\n');
                console.log(`[SourceBuilder] 虚拟模块内容:\n${imports}`);
                return imports || '// No user scripts';
            }
            return null;
        },
        async closeBundle() {
            if (!projectConfig) {
                return;
            }
            // 确保输出目录存在
            await fs.mkdir(options.outputDir, { recursive: true });
            // 生成 config.json（支持场景过滤）
            const config = await generateConfig(projectConfig, {
                selectedScenes: options.selectedScenes,
                projectDir: options.projectDir,
            });
            // 为脚本资产补齐 __game-scripts.js 信息（仅在 Classic 模式下）
            if (options.buildMode !== 'esm') {
                await patchScriptAssets(config, options.outputDir);
            }
            await fs.writeFile(path.join(options.outputDir, 'config.json'), JSON.stringify(config, null, 2), 'utf-8');
            console.log('[SourceBuilder] 生成 config.json');
            // 生成 __settings__.js
            // 如果 config.scenes 中有场景，使用第一个场景的 URL
            const firstScenePath = config.scenes && config.scenes.length > 0
                ? config.scenes[0].url
                : undefined;
            const settings = generateSettings(projectConfig, firstScenePath);
            await fs.writeFile(path.join(options.outputDir, '__settings__.js'), settings, 'utf-8');
            console.log('[SourceBuilder] 生成 __settings__.js');
            // 复制模板文件和生成 HTML
            const buildMode = options.buildMode || 'classic';
            if (buildMode === 'esm') {
                await generateESMTemplate(projectConfig, options, config);
                console.log('[SourceBuilder] 生成 ESM 模板');
            }
            else {
                await copyTemplates(options.outputDir);
                console.log('[SourceBuilder] 复制传统模板文件');
            }
            // 复制资源文件（ESM 模式下跳过脚本，因为脚本由 generateESMTemplate 中的 copyUserScriptsForESM 处理）
            const skipScripts = buildMode === 'esm';
            await copyAssets(projectConfig, options.projectDir, options.outputDir, config.assets, skipScripts);
            console.log('[SourceBuilder] 复制资源文件');
            // 生成场景文件（只生成 config 中包含的场景）
            await generateSceneFiles(projectConfig, options.projectDir, options.outputDir, config.scenes);
            console.log('[SourceBuilder] 生成场景文件');
        },
    };
}
/**
 * 检测项目格式
 */
async function detectFormat(projectDir) {
    // 检查 manifest.json（PlayCraft）
    const manifestPath = path.join(projectDir, 'manifest.json');
    try {
        const content = await fs.readFile(manifestPath, 'utf-8');
        const manifest = JSON.parse(content);
        if (manifest.format === 'playcraft' || Array.isArray(manifest.assets)) {
            return 'playcraft';
        }
    }
    catch (error) {
        // manifest.json 不存在或格式不对
    }
    // 默认为 PlayCanvas
    return 'playcanvas';
}
/**
 * 构建脚本路径映射（基于文件夹层次结构）
 */
function buildScriptPathMapping(projectConfig, userScripts, projectDir) {
    const moduleToFile = new Map();
    const fileToModule = new Map();
    const assets = projectConfig.format === 'playcanvas'
        ? projectConfig.assets
        : projectConfig.assets;
    // 构建文件夹 ID 到名称的映射
    const folderMap = new Map();
    for (const [assetId, asset] of Object.entries(assets)) {
        const assetData = asset;
        if (assetData.type === 'folder') {
            folderMap.set(assetId, assetData.name);
        }
    }
    // 为每个脚本构建模块路径
    for (const script of userScripts) {
        const asset = assets[script.id];
        if (!asset)
            continue;
        const pathIds = asset.path || [];
        const pathNames = [];
        // 跳过根文件夹（第一个元素），构建路径
        for (let i = 1; i < pathIds.length; i++) {
            const folderId = String(pathIds[i]);
            const folderName = folderMap.get(folderId);
            if (folderName) {
                pathNames.push(folderName);
            }
        }
        // 添加文件名
        const filename = asset.name || asset.file?.filename;
        if (filename) {
            pathNames.push(filename);
        }
        const modulePath = pathNames.join('/');
        const fullPath = path.join(projectDir, script.path);
        // 添加双向映射
        moduleToFile.set(modulePath, fullPath);
        fileToModule.set(fullPath, modulePath);
        // 同时添加不带扩展名的映射
        const withoutExt = modulePath.replace(/\.mjs$/, '');
        moduleToFile.set(withoutExt, fullPath);
    }
    return { moduleToFile, fileToModule };
}
/**
 * 收集用户脚本
 */
async function collectUserScripts(projectConfig, projectDir) {
    const scripts = [];
    if (projectConfig.format === 'playcanvas') {
        const pcProject = projectConfig;
        const scriptIds = pcProject.project.settings?.scripts || [];
        // 从 assets.json 读取脚本资产
        for (const scriptId of scriptIds) {
            const asset = pcProject.assets[String(scriptId)];
            if (asset && asset.type === 'script') {
                const assetData = asset;
                // 检查 preload 属性（默认为 true）
                const preload = assetData.preload !== false;
                if (!preload) {
                    continue; // 跳过 non-preload 脚本
                }
                if (assetData.file?.url && !assetData.file.url.startsWith('data:')) {
                    const scriptPath = path.join(projectDir, assetData.file.url);
                    try {
                        const code = await fs.readFile(scriptPath, 'utf-8');
                        scripts.push({
                            id: String(scriptId),
                            path: assetData.file.url,
                            code,
                            name: assetData.name || `script_${scriptId}`,
                        });
                    }
                    catch (error) {
                        console.warn(`警告: 无法读取脚本文件: ${scriptPath}`);
                    }
                }
            }
        }
    }
    else {
        // PlayCraft 格式
        const pcProject = projectConfig;
        const scriptIds = pcProject.manifest.settings?.scripts || [];
        for (const scriptId of scriptIds) {
            const asset = pcProject.assets[String(scriptId)];
            if (asset && asset.type === 'script') {
                const assetData = asset;
                // 检查 preload 属性（默认为 true）
                const preload = assetData.preload !== false;
                if (!preload) {
                    continue; // 跳过 non-preload 脚本
                }
                if (assetData.file?.url && !assetData.file.url.startsWith('data:')) {
                    const scriptPath = path.join(projectDir, assetData.file.url);
                    try {
                        const code = await fs.readFile(scriptPath, 'utf-8');
                        scripts.push({
                            id: String(scriptId),
                            path: assetData.file.url,
                            code,
                            name: assetData.name || `script_${scriptId}`,
                        });
                    }
                    catch (error) {
                        console.warn(`警告: 无法读取脚本文件: ${scriptPath}`);
                    }
                }
            }
        }
    }
    return scripts;
}
/**
 * 复制模板文件
 */
async function copyTemplates(outputDir) {
    const templatesDir = path.resolve(__dirname, '../../templates');
    const templateFiles = [
        '__start__.js',
        '__modules__.js',
        '__loading__.js',
        'playcanvas-stable.min.js',
        'styles.css',
        'manifest.json',
    ];
    for (const file of templateFiles) {
        const sourcePath = path.join(templatesDir, file);
        const targetPath = path.join(outputDir, file);
        try {
            await fs.copyFile(sourcePath, targetPath);
        }
        catch (error) {
            console.warn(`警告: 无法复制模板文件 ${file}:`, error);
        }
    }
    // 生成 index.html
    const projectName = 'PlayCanvas Project';
    const htmlTemplate = await fs.readFile(path.join(templatesDir, 'index.html'), 'utf-8');
    const htmlContent = htmlTemplate.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
    await fs.writeFile(path.join(outputDir, 'index.html'), htmlContent, 'utf-8');
}
/**
 * 复制资源文件
 */
async function copyAssets(projectConfig, projectDir, outputDir, assetsOverride, skipScripts // ESM 模式下跳过脚本（脚本由 copyUserScriptsForESM 处理）
) {
    const assetsOutputDir = path.join(outputDir, 'files');
    await fs.mkdir(assetsOutputDir, { recursive: true });
    const assets = assetsOverride || (projectConfig.format === 'playcanvas'
        ? projectConfig.assets
        : projectConfig.assets);
    // 使用 Set 去重，避免重复复制同一个文件
    const copiedUrls = new Set();
    let copiedCount = 0;
    let failedCount = 0;
    // 遍历 assets
    for (const [, asset] of Object.entries(assets)) {
        const assetData = asset;
        if (!assetData.file?.url || assetData.file.url.startsWith('data:')) {
            continue;
        }
        // ESM 模式下跳过脚本类型的资源（脚本由 copyUserScriptsForESM 单独处理）
        if (skipScripts && assetData.type === 'script') {
            continue;
        }
        // 跳过已经复制过的 URL
        if (copiedUrls.has(assetData.file.url)) {
            continue;
        }
        const sourcePath = path.join(projectDir, assetData.file.url);
        const targetPath = path.join(outputDir, assetData.file.url);
        try {
            // 检查源文件是否存在
            await fs.access(sourcePath);
            // 创建目标目录
            const targetDir = path.dirname(targetPath);
            await fs.mkdir(targetDir, { recursive: true });
            // 复制文件
            await fs.copyFile(sourcePath, targetPath);
            // 标记为已复制
            copiedUrls.add(assetData.file.url);
            copiedCount++;
        }
        catch (error) {
            // 资源文件可能不存在（可能是内联的）
            failedCount++;
        }
    }
    console.log(`[SourceBuilder] 复制资源文件: ${copiedCount} 个成功${failedCount > 0 ? `, ${failedCount} 个失败` : ''}`);
}
/**
 * 收集项目中所有已注册的脚本名称
 * 从脚本资产中提取 scriptName（ESM 脚本通过 static scriptName 定义）
 */
function collectRegisteredScriptNames(assets) {
    const scriptNames = new Set();
    for (const [, asset] of Object.entries(assets)) {
        if (asset?.type !== 'script')
            continue;
        // 脚本名称可以从多个来源获取
        // 1. ESM 脚本：从 data.scriptName 或文件名推导
        // 2. Classic 脚本：从 name 字段或 file.filename 推导
        const name = asset.name || '';
        const filename = asset.file?.filename || '';
        // 从文件名推导脚本名（移除扩展名，首字母小写）
        let scriptName = '';
        if (name) {
            // 移除 .mjs/.js 扩展名
            const baseName = name.replace(/\.(mjs|js)$/i, '');
            scriptName = baseName.charAt(0).toLowerCase() + baseName.slice(1);
        }
        else if (filename) {
            const baseName = filename.replace(/\.(mjs|js)$/i, '');
            scriptName = baseName.charAt(0).toLowerCase() + baseName.slice(1);
        }
        if (scriptName) {
            scriptNames.add(scriptName);
        }
        // Classic 格式的 data.scripts 可能是对象或数组
        if (asset.data?.scripts) {
            if (Array.isArray(asset.data.scripts)) {
                // 数组格式: [{ name: 'xxx' }, ...]
                for (const s of asset.data.scripts) {
                    if (s?.name)
                        scriptNames.add(s.name);
                }
            }
            else if (typeof asset.data.scripts === 'object') {
                // 对象格式: { "followCamera": { attributes: {...} }, ... }
                // 这是 PlayCanvas/PlayCraft 中 Classic 脚本的标准格式
                for (const scriptKey of Object.keys(asset.data.scripts)) {
                    scriptNames.add(scriptKey);
                }
            }
        }
    }
    return scriptNames;
}
/**
 * 验证并清理场景中的无效脚本引用
 * 移除引用了不存在脚本的组件
 */
function validateAndCleanSceneScripts(sceneData, registeredScripts, _sceneName // 保留参数用于将来的日志输出
) {
    const removedScripts = [];
    if (!sceneData?.entities) {
        return { cleaned: sceneData, removedScripts };
    }
    // 深拷贝场景数据以避免修改原始数据
    const cleaned = JSON.parse(JSON.stringify(sceneData));
    for (const [entityId, entity] of Object.entries(cleaned.entities)) {
        const scriptComp = entity?.components?.script;
        if (!scriptComp)
            continue;
        // 处理 ESM 格式：scripts 是对象 { scriptName: {...}, ... }
        if (scriptComp.scripts && typeof scriptComp.scripts === 'object' && !Array.isArray(scriptComp.scripts)) {
            const validScripts = {};
            for (const [scriptName, scriptData] of Object.entries(scriptComp.scripts)) {
                if (registeredScripts.has(scriptName)) {
                    validScripts[scriptName] = scriptData;
                }
                else {
                    removedScripts.push(`${entity.name || entityId}/${scriptName}`);
                }
            }
            // BUG: Preserve original order: filter based on the original order array, not scripts object key order
            const validOrder = (scriptComp.order || []).filter((name) => name in validScripts);
            scriptComp.scripts = validScripts;
            scriptComp.order = validOrder;
            // 如果所有脚本都被移除，删除整个 script 组件
            if (Object.keys(validScripts).length === 0) {
                delete entity.components.script;
            }
        }
        // 处理 Classic 格式：scripts 是数组 [{ name: 'xxx' }, ...]
        if (scriptComp.scripts && Array.isArray(scriptComp.scripts)) {
            const validScripts = scriptComp.scripts.filter((s) => {
                if (!s?.name)
                    return false;
                if (registeredScripts.has(s.name))
                    return true;
                removedScripts.push(`${entity.name || entityId}/${s.name}`);
                return false;
            });
            scriptComp.scripts = validScripts;
            if (validScripts.length === 0) {
                delete entity.components.script;
            }
        }
    }
    return { cleaned, removedScripts };
}
/**
 * 生成场景文件
 */
async function generateSceneFiles(projectConfig, _projectDir, // 保留参数以保持 API 兼容性
outputDir, selectedScenes) {
    // 收集已注册的脚本名称
    const assets = projectConfig.format === 'playcanvas'
        ? projectConfig.assets || {}
        : projectConfig.assets || {};
    const registeredScripts = collectRegisteredScriptNames(assets);
    console.log(`[SceneGenerator] 已注册脚本数量: ${registeredScripts.size}`);
    if (projectConfig.format === 'playcanvas') {
        const pcProject = projectConfig;
        // PlayCanvas 格式：scenes.json 中已包含场景数据
        if (pcProject.scenes) {
            let scenes = Array.isArray(pcProject.scenes)
                ? pcProject.scenes
                : Object.entries(pcProject.scenes).map(([keyId, scene]) => {
                    // keyId 是场景文件名使用的 ID（如 "2388047"）
                    return { ...scene, sceneFileId: keyId };
                });
            // 如果提供了 selectedScenes，只生成这些场景
            if (selectedScenes && selectedScenes.length > 0) {
                const selectedUrls = new Set(selectedScenes.map(s => s.url));
                scenes = scenes.filter((scene) => {
                    // 优先使用 sceneFileId（来自 scenes.json 的 key）
                    const sceneId = scene.sceneFileId || scene.id || scene.name || 'scene';
                    const sceneUrl = `${sceneId}.json`;
                    return selectedUrls.has(sceneUrl);
                });
            }
            let totalRemoved = 0;
            for (const scene of scenes) {
                const sceneData = scene;
                // 优先使用 sceneFileId（来自 scenes.json 的 key）
                const sceneId = sceneData.sceneFileId || sceneData.id || sceneData.name || 'scene';
                const scenePath = path.join(outputDir, `${sceneId}.json`);
                // 验证并清理无效脚本引用
                const { cleaned, removedScripts } = validateAndCleanSceneScripts(sceneData, registeredScripts, sceneData.name || sceneId);
                if (removedScripts.length > 0) {
                    totalRemoved += removedScripts.length;
                    console.warn(`\n⚠️  场景 "${sceneData.name || sceneId}" 中发现 ${removedScripts.length} 个无效脚本引用，已移除:`);
                    removedScripts.slice(0, 10).forEach(s => console.warn(`    - ${s}`));
                    if (removedScripts.length > 10) {
                        console.warn(`    ... 还有 ${removedScripts.length - 10} 个`);
                    }
                }
                // 写入清理后的场景 JSON
                await fs.writeFile(scenePath, JSON.stringify(cleaned, null, 2), 'utf-8');
            }
            if (totalRemoved > 0) {
                console.log(`\n📋 共移除 ${totalRemoved} 个无效脚本引用`);
            }
        }
    }
    else {
        // PlayCraft 格式：从 scenes/ 目录读取
        const pcProject = projectConfig;
        let scenes = pcProject.scenes;
        // 如果提供了 selectedScenes，只生成这些场景
        if (selectedScenes && selectedScenes.length > 0) {
            const selectedNames = new Set(selectedScenes.map(s => s.name));
            scenes = scenes.filter((scene) => {
                const sceneName = scene.name || scene.id || 'scene';
                return selectedNames.has(sceneName);
            });
        }
        let totalRemoved = 0;
        for (const scene of scenes) {
            const sceneData = scene;
            const sceneId = sceneData.id || sceneData.name || 'scene';
            const scenePath = path.join(outputDir, `${sceneId}.json`);
            // 验证并清理无效脚本引用
            const { cleaned, removedScripts } = validateAndCleanSceneScripts(sceneData, registeredScripts, sceneData.name || sceneId);
            if (removedScripts.length > 0) {
                totalRemoved += removedScripts.length;
                console.warn(`\n⚠️  场景 "${sceneData.name || sceneId}" 中发现 ${removedScripts.length} 个无效脚本引用，已移除:`);
                removedScripts.slice(0, 10).forEach(s => console.warn(`    - ${s}`));
                if (removedScripts.length > 10) {
                    console.warn(`    ... 还有 ${removedScripts.length - 10} 个`);
                }
            }
            // 写入清理后的场景 JSON
            await fs.writeFile(scenePath, JSON.stringify(cleaned, null, 2), 'utf-8');
        }
        if (totalRemoved > 0) {
            console.log(`\n📋 共移除 ${totalRemoved} 个无效脚本引用`);
        }
    }
}
/**
 * 构建文件夹 ID -> 完整目录路径的映射
 * PlayCraft Git 项目的 assets.json 中，资产的 path 字段是文件夹 ID 数组（如 [30097022, 30097024]），
 * 需要通过递归遍历构建每个 folder ID 对应的完整路径（如 "LiteCreator/System"）
 */
function buildFolderFullPathMap(assets) {
    const folderMap = new Map();
    const buildPath = (folderId, visited = new Set()) => {
        if (visited.has(folderId))
            return '';
        if (folderMap.has(folderId))
            return folderMap.get(folderId);
        visited.add(folderId);
        const folder = assets[folderId];
        if (!folder || folder.type !== 'folder')
            return '';
        const pathIds = folder.path || [];
        if (pathIds.length === 0) {
            folderMap.set(folderId, folder.name);
            return folder.name;
        }
        // 最后一个 ID 是直接父文件夹
        const parentId = String(pathIds[pathIds.length - 1]);
        const parentPath = buildPath(parentId, visited);
        const fullPath = parentPath ? `${parentPath}/${folder.name}` : folder.name;
        folderMap.set(folderId, fullPath);
        return fullPath;
    };
    for (const [id, asset] of Object.entries(assets)) {
        if (asset.type === 'folder') {
            buildPath(id);
        }
    }
    return folderMap;
}
/**
 * 从资产的 path 数组（文件夹 ID 列表）重建 treePath
 * path 数组中最后一个 ID 是直接父文件夹
 */
function resolveTreePathFromPathArray(pathIds, folderFullPathMap) {
    if (!pathIds || !Array.isArray(pathIds) || pathIds.length === 0) {
        return '';
    }
    const parentFolderId = String(pathIds[pathIds.length - 1]);
    return folderFullPathMap.get(parentFolderId) || '';
}
/**
 * 规范化资源 URL（补齐 file.url）
 * 源码导出中的资源只有 file.filename，没有 file.url
 * 需要根据 assetId 和 revision 构建完整的文件路径
 */
async function normalizeAssetUrls(projectConfig, projectDir) {
    const assets = projectConfig.format === 'playcanvas'
        ? projectConfig.assets
        : projectConfig.assets;
    // 构建文件夹 ID -> 完整路径的映射（用于从 path 数组重建 treePath）
    const folderFullPathMap = buildFolderFullPathMap(assets);
    let normalizedCount = 0;
    let notFoundCount = 0;
    for (const [assetId, asset] of Object.entries(assets)) {
        const assetData = asset;
        // 跳过没有 file 的资源
        if (!assetData.file) {
            continue;
        }
        // 如果已有有效的本地 url（非 API URL），验证文件是否存在
        if (assetData.file.url && !assetData.file.url.startsWith('/api/') && !assetData.file.url.startsWith('http')) {
            // 已有有效的本地 url，验证文件存在性
            try {
                await fs.access(path.join(projectDir, assetData.file.url));
                continue; // 文件存在，跳过
            }
            catch {
                // 文件不存在，清除 url 继续搜索
                delete assetData.file.url;
            }
        }
        // 如果 url 是 API URL（PlayCraft 格式残留），清除它
        if (assetData.file.url && (assetData.file.url.startsWith('/api/') || assetData.file.url.startsWith('http'))) {
            delete assetData.file.url;
        }
        // 如果已经有有效 url 了（由之前的 loader 设置的），跳过
        if (assetData.file.url) {
            continue;
        }
        const filename = assetData.file.filename;
        if (!filename) {
            continue;
        }
        const revision = assetData.revision ?? assetData.file.revision ?? 1;
        const candidates = [
            // PlayCanvas 标准结构
            path.join('files', 'assets', String(assetId), String(revision), filename),
            path.join('files', 'assets', String(assetId), '1', filename),
            path.join('files', 'assets', String(assetId), filename),
            path.join('files', filename),
        ];
        // PlayCraft 结构：使用 treePath 推断文件位置
        if (assetData.treePath) {
            candidates.push(path.join('assets', assetData.treePath, filename));
            candidates.push(path.join(assetData.treePath, filename));
        }
        // PlayCraft Git 仓库结构：从 path 数组（文件夹 ID 列表）重建 treePath
        if (assetData.path && Array.isArray(assetData.path) && assetData.path.length > 0) {
            const computedTreePath = resolveTreePathFromPathArray(assetData.path, folderFullPathMap);
            if (computedTreePath) {
                candidates.push(path.join('assets', computedTreePath, filename));
                candidates.push(path.join(computedTreePath, filename));
            }
        }
        // 最后尝试直接在 assets/ 根目录下查找（顶层文件）
        candidates.push(path.join('assets', filename));
        let found = false;
        for (const rel of candidates) {
            const abs = path.join(projectDir, rel);
            try {
                await fs.access(abs);
                assetData.file.url = rel;
                normalizedCount++;
                found = true;
                break;
            }
            catch (error) {
                // 继续尝试下一个
            }
        }
        if (!found) {
            notFoundCount++;
        }
    }
    console.log(`[SourceBuilder] 规范化资源 URL: ${normalizedCount} 个成功${notFoundCount > 0 ? `, ${notFoundCount} 个未找到` : ''}`);
}
/**
 * 为脚本资产补齐 __game-scripts.js 信息
 */
async function patchScriptAssets(config, outputDir) {
    if (!config?.assets) {
        return;
    }
    const scriptIds = Array.isArray(config?.application_properties?.scripts)
        ? config.application_properties.scripts.map((id) => String(id))
        : [];
    if (scriptIds.length === 0) {
        return;
    }
    const gameScriptsPath = path.join(outputDir, '__game-scripts.js');
    let stat = null;
    let hash = null;
    try {
        const buffer = await fs.readFile(gameScriptsPath);
        stat = { size: buffer.byteLength };
        hash = crypto.createHash('md5').update(buffer).digest('hex');
    }
    catch (error) {
        // __game-scripts.js 可能不存在
    }
    for (const [assetId, asset] of Object.entries(config.assets)) {
        const assetData = asset;
        if (!scriptIds.includes(String(assetId))) {
            continue;
        }
        if (assetData.type !== 'script' || !assetData.file) {
            continue;
        }
        assetData.file.url = '__game-scripts.js';
        assetData.file.filename = '__game-scripts.js';
        if (stat && hash) {
            assetData.file.size = stat.size;
            assetData.file.hash = hash;
        }
    }
}
/**
 * Analyze script file import dependencies and add missing dependency scripts to config.assets.
 * This handles cases like: BoardRandomFiller.mjs imports ./BoardShapeGenerator.mjs,
 * which is not directly referenced in the scene or templates.
 */
async function collectScriptImportDependencies(projectConfig, projectDir, importMap, configAssets) {
    const assets = projectConfig.format === 'playcanvas'
        ? projectConfig.assets
        : projectConfig.assets;
    // Build folder ID -> name mapping
    const folderMap = new Map();
    for (const [assetId, asset] of Object.entries(assets)) {
        if (asset.type === 'folder') {
            folderMap.set(assetId, asset.name);
        }
    }
    // Build reverse mapping: targetPath -> assetId (for ALL script assets, not just configAssets)
    const targetPathToAssetId = new Map();
    for (const [assetId, asset] of Object.entries(assets)) {
        const assetData = asset;
        if (assetData.type !== 'script')
            continue;
        const filename = assetData.file?.filename || assetData.name + '.mjs';
        const pathIds = assetData.path || [];
        const folderPath = buildScriptFolderPath(pathIds, folderMap);
        const targetPath = resolveTargetPathFromImportMap(folderPath, filename, importMap);
        if (targetPath) {
            targetPathToAssetId.set(targetPath.replace(/\\/g, '/'), assetId);
        }
    }
    // Regex to extract import specifiers from script content
    const importRegex = /(from\s+['"])([^'"]+)(['"])|(\bimport\s*\(\s*['"])([^'"]+)(['"]\s*\))/g;
    const importsMap = importMap.content.imports || {};
    // Iteratively discover new script dependencies
    const processedScripts = new Set();
    let newDepsFound = true;
    while (newDepsFound) {
        newDepsFound = false;
        const currentScriptIds = Object.keys(configAssets).filter(id => configAssets[id].type === 'script' && !processedScripts.has(id));
        for (const scriptId of currentScriptIds) {
            processedScripts.add(scriptId);
            const assetData = assets[scriptId];
            if (!assetData)
                continue;
            // Resolve source file path
            const sourcePath = await resolveScriptSourcePath(assetData, scriptId, projectDir);
            if (!sourcePath)
                continue;
            let content;
            try {
                content = await fs.readFile(sourcePath, 'utf-8');
            }
            catch {
                continue;
            }
            // Get current script's target path for relative path resolution
            const filename = assetData.file?.filename || assetData.name + '.mjs';
            const pathIds = assetData.path || [];
            const folderPath = buildScriptFolderPath(pathIds, folderMap);
            const scriptTargetPath = resolveTargetPathFromImportMap(folderPath, filename, importMap);
            if (!scriptTargetPath)
                continue;
            const scriptDir = path.dirname(scriptTargetPath).replace(/\\/g, '/');
            // Parse all import statements
            let match;
            importRegex.lastIndex = 0;
            while ((match = importRegex.exec(content)) !== null) {
                const modulePath = match[2] || match[5];
                if (!modulePath)
                    continue;
                let resolvedTargetPath = null;
                if (modulePath.startsWith('./') || modulePath.startsWith('../')) {
                    // Relative import: resolve against script's directory
                    const joined = path.posix.join(scriptDir, modulePath);
                    resolvedTargetPath = path.posix.normalize(joined);
                }
                else if (!modulePath.startsWith('/') && !modulePath.startsWith('http')) {
                    // Import Map alias: resolve via import map
                    resolvedTargetPath = resolveModulePathViaImportMap(modulePath, importsMap);
                }
                if (!resolvedTargetPath)
                    continue;
                resolvedTargetPath = resolvedTargetPath.replace(/\\/g, '/');
                // Look up the asset by target path
                const depAssetId = targetPathToAssetId.get(resolvedTargetPath);
                if (depAssetId === undefined) {
                    console.warn(`Warning: resolvedTargetPath '${resolvedTargetPath}' not found in targetPathToAssetId`);
                }
                else if (!configAssets[depAssetId]) {
                    // Found a missing dependency — add it to configAssets
                    configAssets[depAssetId] = assets[depAssetId];
                    newDepsFound = true;
                    console.log(`📜 [ScriptDeps] ${assetData.name} imports ${assets[depAssetId].name} (${depAssetId}) — added to configAssets`);
                }
            }
        }
    }
}
/**
 * Resolve the source file path for a script asset.
 * @param assets - 可选，所有资产的 map，用于从 path 数组重建 treePath（PlayCraft Git 仓库场景）
 */
async function resolveScriptSourcePath(assetData, scriptId, projectDir, assets) {
    if (assetData.file?.url) {
        const fullPath = path.join(projectDir, assetData.file.url);
        try {
            await fs.access(fullPath);
            return fullPath;
        }
        catch {
            // file.url 指向的文件不存在，继续搜索
        }
    }
    if (assetData.file?.filename) {
        const filename = assetData.file.filename;
        const revision = assetData.revision ?? 1;
        const candidates = [
            // PlayCanvas 标准结构
            path.join(projectDir, 'files', 'assets', String(scriptId), String(revision), filename),
            path.join(projectDir, 'files', 'assets', String(scriptId), '1', filename),
            path.join(projectDir, 'files', 'assets', String(scriptId), filename),
        ];
        // PlayCraft 结构：使用 treePath
        if (assetData.treePath) {
            candidates.push(path.join(projectDir, 'assets', assetData.treePath, filename));
            candidates.push(path.join(projectDir, assetData.treePath, filename));
        }
        // PlayCraft Git 仓库：从 path 数组重建 treePath
        if (assets && assetData.path && Array.isArray(assetData.path) && assetData.path.length > 0) {
            const folderFullPathMap = buildFolderFullPathMap(assets);
            const computedTreePath = resolveTreePathFromPathArray(assetData.path, folderFullPathMap);
            if (computedTreePath) {
                candidates.push(path.join(projectDir, 'assets', computedTreePath, filename));
                candidates.push(path.join(projectDir, computedTreePath, filename));
            }
        }
        // 最后尝试直接在 assets/ 根目录下查找
        candidates.push(path.join(projectDir, 'assets', filename));
        for (const candidate of candidates) {
            try {
                await fs.access(candidate);
                return candidate;
            }
            catch {
                // try next
            }
        }
    }
    return null;
}
/**
 * 生成 ESM 模板文件
 */
async function generateESMTemplate(projectConfig, options, config) {
    const templatesDir = path.resolve(__dirname, '../../templates');
    const outputDir = options.outputDir;
    // 1. 生成 index.html (修复 Import Map 路径)
    const htmlTemplate = await fs.readFile(path.join(templatesDir, 'index.esm.html'), 'utf-8');
    const projectName = projectConfig.format === 'playcanvas'
        ? projectConfig.project.name || 'PlayCanvas Project'
        : projectConfig.manifest.name || 'PlayCanvas Project';
    // 修复 Import Map 中的路径（将 ./Lib/ 改为 ./）
    const fixedImportMap = fixImportMapPaths(options.importMap?.content || { imports: {} });
    const importMapJson = JSON.stringify(fixedImportMap, null, 2);
    const html = htmlTemplate
        .replace(/\{\{PROJECT_NAME\}\}/g, projectName)
        .replace(/\{\{IMPORT_MAP\}\}/g, importMapJson);
    await fs.writeFile(path.join(outputDir, 'index.html'), html, 'utf-8');
    // 2. 复制 PlayCanvas Engine（UMD 格式）和 ESM 包装器
    const jsDir = path.join(outputDir, 'js');
    await fs.mkdir(jsDir, { recursive: true });
    // 复制 UMD 引擎
    const engineSourcePath = path.join(templatesDir, 'playcanvas-stable.min.js');
    const engineTargetPath = path.join(jsDir, 'playcanvas-engine-umd.js');
    try {
        await fs.copyFile(engineSourcePath, engineTargetPath);
        console.log('[SourceBuilder] PlayCanvas Engine (UMD) 已复制');
    }
    catch (error) {
        console.warn('[SourceBuilder] 警告: 无法复制 PlayCanvas Engine:', error);
    }
    // 复制 ESM 包装器
    const wrapperSourcePath = path.join(templatesDir, 'playcanvas-esm-wrapper.mjs');
    const wrapperTargetPath = path.join(jsDir, 'playcanvas-engine.mjs');
    try {
        await fs.copyFile(wrapperSourcePath, wrapperTargetPath);
        console.log('[SourceBuilder] PlayCanvas ESM 包装器已复制');
    }
    catch (error) {
        console.warn('[SourceBuilder] 警告: 无法复制 ESM 包装器:', error);
    }
    // 3. 分析脚本文件的 import 依赖，将缺失的依赖脚本添加到 config.assets
    if (options.importMap) {
        await collectScriptImportDependencies(projectConfig, options.projectDir, options.importMap, config.assets);
    }
    // 4. 收集用户脚本的导入路径（传入 config.assets 以包含 template 依赖 + import 依赖的脚本）
    const scriptImportPaths = await collectESMScriptImports(projectConfig, options, config.assets);
    // 5. 从 Import Map 中提取 gameRule 路径
    let gameRulePath = '';
    if (options.importMap?.content?.imports) {
        const imports = options.importMap.content.imports;
        if (imports['gameRule']) {
            // 将 "./Generated/..." 格式转换为相对于 js/ 目录的路径 "../Generated/..."
            gameRulePath = imports['gameRule'];
            if (gameRulePath.startsWith('./')) {
                gameRulePath = '..' + gameRulePath.slice(1); // "./" -> "../"
            }
            console.log(`[SourceBuilder] GameRule 路径: ${gameRulePath}`);
        }
    }
    // 5.1 从场景 SystemManager 的 GameConfig.json 中收集 Rule 脚本
    if (options.importMap && projectConfig.format === 'playcanvas') {
        const allAssets = projectConfig.assets || {};
        const pcScenes = projectConfig.scenes;
        // Iterate over selected scenes (from config.scenes) and find the scene data
        for (const sceneRef of (config.scenes || [])) {
            // Extract scene file ID from url (e.g. "4577025.json" -> "4577025")
            const sceneFileId = (sceneRef.url || '').replace(/\.json$/, '');
            const sceneData = pcScenes?.[sceneFileId];
            if (!sceneData?.entities)
                continue;
            // Find SystemManager entity
            for (const [, entity] of Object.entries(sceneData.entities)) {
                if (entity?.name !== 'SystemManager')
                    continue;
                const smScript = entity.components?.script?.scripts?.systemManager;
                if (!smScript?.attributes?.gameConfigAsset)
                    continue;
                const jsonAssetId = String(smScript.attributes.gameConfigAsset);
                const jsonAsset = allAssets[jsonAssetId];
                if (!jsonAsset || jsonAsset.type !== 'json')
                    continue;
                // Read the GameConfig JSON file
                const jsonFilePath = await resolveScriptSourcePath(jsonAsset, jsonAssetId, options.projectDir, allAssets);
                if (!jsonFilePath)
                    continue;
                let gameConfig;
                try {
                    const content = await fs.readFile(jsonFilePath, 'utf-8');
                    gameConfig = JSON.parse(content);
                }
                catch {
                    console.warn(`[SourceBuilder] 无法读取 GameConfig.json (${jsonAssetId})`);
                    continue;
                }
                const ruleName = gameConfig?.Gameplay?.Rule;
                if (!ruleName)
                    continue;
                // Resolve Rule script: look for a script asset named "{ruleName}.mjs"
                const ruleFileName = `${ruleName}.mjs`;
                let ruleAssetId = null;
                for (const [assetId, asset] of Object.entries(allAssets)) {
                    if (asset.type === 'script' && (asset.file?.filename === ruleFileName || asset.name === ruleFileName)) {
                        ruleAssetId = assetId;
                        break;
                    }
                }
                if (ruleAssetId && !config.assets[ruleAssetId]) {
                    config.assets[ruleAssetId] = allAssets[ruleAssetId];
                    console.log(`📜 [GameConfig] Rule "${ruleName}" → 资源 ${ruleAssetId} — added to configAssets`);
                    // Also collect its import dependencies
                    if (options.importMap) {
                        await collectScriptImportDependencies(projectConfig, options.projectDir, options.importMap, config.assets);
                    }
                    // Rebuild script import paths to include the new Rule script
                    const newPaths = await collectESMScriptImports(projectConfig, options, config.assets);
                    for (const p of newPaths) {
                        if (!scriptImportPaths.includes(p)) {
                            scriptImportPaths.push(p);
                        }
                    }
                }
                else if (ruleAssetId) {
                    console.log(`📜 [GameConfig] Rule "${ruleName}" → 资源 ${ruleAssetId} — already in configAssets`);
                }
                else {
                    console.warn(`⚠️ [GameConfig] Rule "${ruleName}" 对应的脚本 ${ruleFileName} 未找到`);
                }
                // Use the rule path from 5.1 if Import Map didn't provide a gameRule mapping (step 5)
                if (ruleAssetId && !gameRulePath) {
                    const ruleAsset = allAssets[ruleAssetId];
                    // Build folder map for path resolution
                    const folderMap = new Map();
                    for (const [aid, a] of Object.entries(allAssets)) {
                        if (a.type === 'folder')
                            folderMap.set(aid, a.name);
                    }
                    const ruleFilename = ruleAsset.file?.filename || ruleAsset.name + '.mjs';
                    const ruleFolderPath = buildScriptFolderPath(ruleAsset.path || [], folderMap);
                    const targetPath = resolveTargetPathFromImportMap(ruleFolderPath, ruleFilename, options.importMap);
                    if (targetPath) {
                        gameRulePath = '../' + targetPath;
                        console.log(`📜 [GameConfig] 使用 5.1 收集的 Rule 路径: ${gameRulePath}`);
                    }
                }
            }
        }
    }
    // 6. 生成 js/index.mjs（包含脚本导入），使用过滤后的 config.scenes
    await generateESMEntry(projectConfig, outputDir, scriptImportPaths, config.scenes, gameRulePath);
    // 7. 在 ESM 模式下，复制用户脚本为独立文件，并更新 config.assets 中的 URL
    if (options.importMap) {
        const scriptUrlMap = await copyUserScriptsForESM(projectConfig, options.projectDir, outputDir, options.importMap, config.assets);
        console.log('[SourceBuilder] 用户脚本已复制为独立 ESM 模块');
        // 更新 config.assets 中脚本的 URL
        if (scriptUrlMap.size > 0 && config.assets) {
            for (const [scriptId, newUrl] of scriptUrlMap) {
                if (config.assets[scriptId] && config.assets[scriptId].file) {
                    config.assets[scriptId].file.url = newUrl;
                }
            }
            // 重新写入 config.json
            await fs.writeFile(path.join(outputDir, 'config.json'), JSON.stringify(config, null, 2), 'utf-8');
            console.log(`[SourceBuilder] 更新了 ${scriptUrlMap.size} 个脚本的 URL`);
        }
        // 8. 复制 Import Map 中引用的第三方库文件
        await copyImportMapLibraries(projectConfig, options.projectDir, outputDir, options.importMap);
    }
    // 9. 复制其他必要的模板文件
    const templateFiles = [
        '__start__.js', // 可能不需要了，但先保留
        '__modules__.js',
        '__loading__.js',
        'styles.css',
        'manifest.json',
    ];
    for (const file of templateFiles) {
        const sourcePath = path.join(templatesDir, file);
        const targetPath = path.join(outputDir, file);
        try {
            await fs.copyFile(sourcePath, targetPath);
        }
        catch (error) {
            // 某些文件可能不存在，忽略
        }
    }
}
/**
 * 修复 Import Map 中的路径
 * 将 ./Lib/xxx.mjs 等错误路径修复为正确的相对路径
 * 并添加必要的模块映射（如 playcanvas）
 */
function fixImportMapPaths(importMap) {
    const fixedImports = {};
    // 添加 playcanvas 引擎映射（脚本中使用 import from 'playcanvas'）
    fixedImports['playcanvas'] = './js/playcanvas-engine.mjs';
    for (const [key, value] of Object.entries(importMap.imports || {})) {
        // 修复 ./Lib/ 路径（通常第三方库文件会被复制到根目录）
        let fixedValue = value;
        if (value.startsWith('./Lib/')) {
            // 将 ./Lib/xxx.mjs 改为 ./xxx.mjs
            fixedValue = './' + value.slice(6);
        }
        fixedImports[key] = fixedValue;
    }
    return { imports: fixedImports };
}
/**
 * 收集 ESM 脚本的导入路径
 * @param configAssets - generateConfig 过滤后的 config.assets，包含 template 依赖的脚本
 */
async function collectESMScriptImports(projectConfig, options, configAssets) {
    // 获取资产（支持 PlayCanvas 和 PlayCraft 两种格式）
    let assets;
    if (projectConfig.format === 'playcanvas') {
        assets = projectConfig.assets;
    }
    else {
        assets = projectConfig.assets;
    }
    // 收集所有需要导入的脚本 ID：
    // 1. config.assets 中的 script 类型资产（已包含场景依赖 + template 依赖的脚本）
    // 2. 回退到 settings.scripts（当没有 configAssets 时）
    const scriptIdsToImport = new Set();
    if (configAssets) {
        console.log(`📜 [DEBUG-ESMImports] 使用 configAssets 收集脚本, configAssets 总数: ${Object.keys(configAssets).length}`);
        for (const [assetId, asset] of Object.entries(configAssets)) {
            if (asset.type === 'script') {
                scriptIdsToImport.add(assetId);
            }
        }
        console.log(`[ESMImports] 从 configAssets 收集到 ${scriptIdsToImport.size} 个脚本`);
    }
    else {
        console.log(`[ESMImports] configAssets 未传入, 回退到 settings.scripts`);
        // Fallback: use settings.scripts
        let scriptIds;
        if (projectConfig.format === 'playcanvas') {
            scriptIds = projectConfig.project.settings?.scripts || [];
        }
        else {
            scriptIds = projectConfig.manifest.settings?.scripts || [];
        }
        for (const id of scriptIds) {
            scriptIdsToImport.add(String(id));
        }
    }
    const importPaths = [];
    // 构建文件夹 ID 到名称的映射
    const folderMap = new Map();
    for (const [assetId, asset] of Object.entries(assets)) {
        const assetData = asset;
        if (assetData.type === 'folder') {
            folderMap.set(assetId, assetData.name);
        }
    }
    // 遍历每个脚本，构建导入路径
    for (const scriptId of scriptIdsToImport) {
        const asset = assets[String(scriptId)];
        if (!asset || asset.type !== 'script')
            continue;
        const assetData = asset;
        // 获取正确的文件名（带扩展名）
        const filename = assetData.file?.filename || assetData.name + '.mjs';
        // 获取脚本的文件夹路径
        const pathIds = assetData.path || [];
        const folderPath = buildScriptFolderPath(pathIds, folderMap);
        // 根据 import map 确定目标路径
        if (options.importMap) {
            const targetPath = resolveTargetPathFromImportMap(folderPath, filename, options.importMap);
            console.log(`[ESM Imports] ${assetData.name}: folderPath=${folderPath}, filename=${filename}, targetPath=${targetPath}`);
            if (targetPath) {
                // 生成相对于 js/ 目录的导入路径
                // 入口文件在 js/index.mjs，脚本在根目录，所以需要 ../
                importPaths.push('../' + targetPath);
            }
        }
    }
    return importPaths;
}
/**
 * 生成 ESM 入口文件 js/index.mjs
 */
async function generateESMEntry(projectConfig, outputDir, scriptImportPaths = [], configScenes, gameRulePath = '') {
    const templatesDir = path.resolve(__dirname, '../../templates');
    const esmTemplate = await fs.readFile(path.join(templatesDir, 'index.esm.mjs'), 'utf-8');
    // 获取项目设置
    let settings;
    let scripts = [];
    if (projectConfig.format === 'playcanvas') {
        const pcProject = projectConfig;
        settings = pcProject.project.settings || {};
        scripts = settings.scripts || [];
    }
    else {
        const pcProject = projectConfig;
        settings = pcProject.manifest.settings || {};
        scripts = settings.scripts || [];
    }
    // 确定场景路径（优先使用传入的 configScenes，否则从原始 projectConfig 获取）
    let scenePath = '';
    if (configScenes && configScenes.length > 0) {
        // 使用过滤后的配置场景
        scenePath = configScenes[0].url;
    }
    else if (projectConfig.format === 'playcanvas') {
        const pcProject = projectConfig;
        if (pcProject.scenes) {
            const scenes = Array.isArray(pcProject.scenes)
                ? pcProject.scenes
                : Object.values(pcProject.scenes);
            if (scenes.length > 0) {
                const firstScene = scenes[0];
                scenePath = firstScene.url || firstScene.file || `${firstScene.id || 'scene'}.json`;
            }
        }
    }
    else {
        const pcProject = projectConfig;
        if (pcProject.scenes && pcProject.scenes.length > 0) {
            const firstScene = pcProject.scenes[0];
            scenePath = firstScene.url || `${firstScene.id || 'scene'}.json`;
        }
    }
    // 构建 CONTEXT_OPTIONS
    const contextOptions = {
        antialias: settings.antiAlias ?? true,
        alpha: false,
        preserveDrawingBuffer: settings.preserveDrawingBuffer ?? false,
        deviceTypes: ['webgl2', 'webgl1'],
        powerPreference: settings.powerPreference || 'high-performance'
    };
    // 构建 INPUT_SETTINGS
    const inputSettings = {
        useKeyboard: settings.useKeyboard ?? true,
        useMouse: settings.useMouse ?? true,
        useGamepads: settings.useGamepads ?? false,
        useTouch: settings.useTouch ?? true
    };
    // 提取预加载模块（WASM 等）
    const preloadModules = [];
    // TODO: 从 assets 中提取 WASM 模块
    // 生成脚本导入路径数组（延迟加载）
    console.log(`[ESMEntry] 生成 ${scriptImportPaths.length} 个脚本导入路径`);
    // 替换模板占位符
    const entry = esmTemplate
        .replace(/\{\{SCRIPT_IMPORT_PATHS\}\}/g, JSON.stringify(scriptImportPaths))
        .replace(/\{\{GAME_RULE_PATH\}\}/g, gameRulePath)
        .replace(/\{\{ASSET_PREFIX\}\}/g, '')
        .replace(/\{\{SCRIPT_PREFIX\}\}/g, '')
        .replace(/\{\{SCENE_PATH\}\}/g, scenePath)
        .replace(/\{\{CONTEXT_OPTIONS\}\}/g, JSON.stringify(contextOptions))
        .replace(/\{\{SCRIPTS\}\}/g, JSON.stringify(scripts))
        .replace(/\{\{INPUT_SETTINGS\}\}/g, JSON.stringify(inputSettings))
        .replace(/\{\{PRELOAD_MODULES\}\}/g, JSON.stringify(preloadModules))
        .replace(/\{\{LEGACY_SCRIPTS\}\}/g, String(settings.useLegacyScripts ?? false));
    // 写入 js/index.mjs
    const jsDir = path.join(outputDir, 'js');
    await fs.mkdir(jsDir, { recursive: true });
    await fs.writeFile(path.join(jsDir, 'index.mjs'), entry, 'utf-8');
}
/**
 * 在 ESM 模式下复制用户脚本为独立文件
 * @param configAssets - generateConfig 过滤后的 config.assets，包含 template 依赖的脚本
 * @returns 脚本ID到新URL的映射
 */
async function copyUserScriptsForESM(projectConfig, projectDir, outputDir, importMap, configAssets) {
    const scriptUrlMap = new Map();
    // 获取资产（支持 PlayCanvas 和 PlayCraft 两种格式）
    let assets;
    if (projectConfig.format === 'playcanvas') {
        assets = projectConfig.assets;
    }
    else {
        assets = projectConfig.assets;
    }
    // 收集所有需要复制的脚本 ID：
    // 1. config.assets 中的 script 类型资产（已包含场景依赖 + template 依赖的脚本）
    // 2. 回退到 settings.scripts（当没有 configAssets 时）
    const scriptIdsToProcess = new Set();
    if (configAssets) {
        for (const [assetId, asset] of Object.entries(configAssets)) {
            if (asset.type === 'script') {
                scriptIdsToProcess.add(assetId);
            }
        }
    }
    else {
        let scriptIds;
        if (projectConfig.format === 'playcanvas') {
            scriptIds = projectConfig.project.settings?.scripts || [];
        }
        else {
            scriptIds = projectConfig.manifest.settings?.scripts || [];
        }
        for (const id of scriptIds) {
            scriptIdsToProcess.add(String(id));
        }
    }
    console.log(`[copyUserScriptsForESM] 复制 ${scriptIdsToProcess.size} 个脚本`);
    // 构建文件夹 ID 到名称的映射
    const folderMap = new Map();
    for (const [assetId, asset] of Object.entries(assets)) {
        const assetData = asset;
        if (assetData.type === 'folder') {
            folderMap.set(assetId, assetData.name);
        }
    }
    // 遍历每个脚本
    for (const scriptId of scriptIdsToProcess) {
        const asset = assets[String(scriptId)];
        if (!asset || asset.type !== 'script')
            continue;
        const assetData = asset;
        // 获取脚本源文件路径（使用统一的解析函数，支持 PlayCraft Git 仓库结构）
        const sourcePath = await resolveScriptSourcePath(assetData, String(scriptId), projectDir, assets);
        if (!sourcePath) {
            console.warn(`[ESM] 无法找到脚本 ${assetData.name} 的源文件，跳过`);
            continue;
        }
        // 获取脚本的文件夹路径
        const pathIds = assetData.path || [];
        const folderPath = buildScriptFolderPath(pathIds, folderMap);
        // 获取正确的文件名（带扩展名）
        const scriptFilename = assetData.file?.filename || assetData.name + '.mjs';
        // 根据 import map 确定目标路径
        const targetPath = resolveTargetPathFromImportMap(folderPath, scriptFilename, importMap);
        if (!targetPath) {
            console.warn(`[ESM] 无法为脚本 ${assetData.name} 确定目标路径，跳过`);
            continue;
        }
        console.log(`[ESM] 脚本 ${assetData.name}: folderPath=${folderPath}, targetPath=${targetPath}`);
        // 复制文件（并重写 import 语句中的 Import Map 别名）
        const outputPath = path.join(outputDir, targetPath);
        try {
            await fs.mkdir(path.dirname(outputPath), { recursive: true });
            // 读取源文件
            let content = await fs.readFile(sourcePath, 'utf-8');
            // 修复已知的代码问题（如未定义变量的错误日志）
            console.log(`[copyUserScriptsForESM] 处理脚本: ${assetData.name}`);
            content = fixKnownCodeIssues(content, assetData.name);
            // 重写 import 语句，将 Import Map 别名转换为相对路径
            content = rewriteImportsWithImportMap(content, targetPath, importMap);
            // 重写 pc.app 访问，确保在 ESM 模式下能正确获取 app 实例
            content = rewritePcAppAccess(content);
            // 写入目标文件
            await fs.writeFile(outputPath, content, 'utf-8');
            // 记录脚本ID到新URL的映射
            scriptUrlMap.set(String(scriptId), targetPath);
        }
        catch (error) {
            console.warn(`[ESM] 无法复制脚本 ${assetData.name}:`, error);
        }
    }
    return scriptUrlMap;
}
/**
 * 重写脚本内容中的 import 语句
 * 将 Import Map 别名转换为相对于脚本位置的相对路径
 */
function rewriteImportsWithImportMap(content, scriptTargetPath, importMap) {
    const imports = importMap.content.imports || {};
    // 获取脚本所在目录（相对于输出根目录）
    const scriptDir = path.dirname(scriptTargetPath);
    // 正则匹配 import 语句中的模块说明符
    // 匹配: import ... from 'xxx' 或 import ... from "xxx" 或 import('xxx') 等
    const importRegex = /(from\s+['"])([^'"]+)(['"])|(\bimport\s*\(\s*['"])([^'"]+)(['"]\s*\))/g;
    return content.replace(importRegex, (match, p1, p2, p3, p4, p5, p6) => {
        const prefix = p1 || p4;
        const modulePath = p2 || p5;
        const suffix = p3 || p6;
        // 如果已经是相对路径（./ 或 ../）或绝对路径（/ 或 http），不处理
        if (modulePath.startsWith('./') || modulePath.startsWith('../') ||
            modulePath.startsWith('/') || modulePath.startsWith('http://') ||
            modulePath.startsWith('https://')) {
            return match;
        }
        // 尝试通过 Import Map 解析模块路径
        const resolvedPath = resolveModulePathViaImportMap(modulePath, imports);
        if (resolvedPath) {
            // 计算从脚本位置到解析后路径的相对路径
            const relativePath = calculateRelativePath(scriptDir, resolvedPath);
            return prefix + relativePath + suffix;
        }
        // 如果无法解析，保持原样
        return match;
    });
}
/**
 * 通过 Import Map 解析模块路径
 * 注意：需要应用与 fixImportMapPaths 相同的路径修复逻辑
 */
function resolveModulePathViaImportMap(modulePath, imports) {
    // 1. 精确匹配
    if (imports[modulePath]) {
        let resolved = imports[modulePath];
        // 去掉开头的 ./
        if (resolved.startsWith('./')) {
            resolved = resolved.slice(2);
        }
        // 修复 Lib/ 路径（与 fixImportMapPaths 保持一致）
        // 将 Lib/xxx.mjs 改为 xxx.mjs（因为这些文件会被复制到根目录）
        if (resolved.startsWith('Lib/')) {
            resolved = resolved.slice(4);
        }
        return resolved;
    }
    // 2. 前缀匹配（目录别名）
    for (const [prefix, target] of Object.entries(imports)) {
        if (prefix.endsWith('/') && modulePath.startsWith(prefix)) {
            const remainder = modulePath.slice(prefix.length);
            let targetBase = target;
            // 去掉开头的 ./
            if (targetBase.startsWith('./')) {
                targetBase = targetBase.slice(2);
            }
            // 修复 Lib/ 路径
            if (targetBase.startsWith('Lib/')) {
                targetBase = targetBase.slice(4);
            }
            // 去掉结尾的 /
            if (targetBase.endsWith('/')) {
                targetBase = targetBase.slice(0, -1);
            }
            return targetBase + '/' + remainder;
        }
    }
    return null;
}
/**
 * 计算从源目录到目标路径的相对路径
 */
function calculateRelativePath(fromDir, toPath) {
    // 将路径统一使用正斜杠
    fromDir = fromDir.replace(/\\/g, '/');
    toPath = toPath.replace(/\\/g, '/');
    // 分割路径
    const fromParts = fromDir ? fromDir.split('/').filter(p => p) : [];
    const toParts = toPath.split('/').filter(p => p);
    // 找到共同前缀
    let commonLength = 0;
    for (let i = 0; i < Math.min(fromParts.length, toParts.length); i++) {
        if (fromParts[i] === toParts[i]) {
            commonLength++;
        }
        else {
            break;
        }
    }
    // 计算需要向上走多少级
    const upCount = fromParts.length - commonLength;
    // 构建相对路径
    const upPath = '../'.repeat(upCount);
    const downPath = toParts.slice(commonLength).join('/');
    const result = upPath + downPath;
    // 如果结果为空或不以 ./ 或 ../ 开头，添加 ./
    if (!result || (!result.startsWith('./') && !result.startsWith('../'))) {
        return './' + result;
    }
    return result;
}
/**
 * 修复已知的代码问题
 * 这些是项目源代码中的 bug，需要在打包时自动修复
 */
function fixKnownCodeIssues(content, scriptName) {
    let modified = content;
    let fixCount = 0;
    // 修复 1: GameplaySystem.mjs 中 ruleName 未定义的问题
    // 原代码: console.error(`[LC] Load game rule '${ruleName}' failed`);
    // 问题: ruleName 变量在上面被注释掉了，但错误日志仍然引用它
    if (content.includes("console.error(`[LC] Load game rule '${ruleName}' failed`)")) {
        modified = modified.replace("console.error(`[LC] Load game rule '${ruleName}' failed`)", "console.error(`[LC] Load game rule failed - GameplaySystem.gameRule is null`)");
        fixCount++;
        console.log(`[SourceBuilder] 修复 ${scriptName}: ruleName 未定义问题`);
    }
    // 修复 2: AimAdjustmentBar.mjs 中 sprite 可能为 null 的问题
    // 原代码: const frameKey = this.wheel.element.sprite.frameKeys[frameNum];
    // 问题: sprite 资源可能还没加载完成或引用丢失
    if (content.includes('this.wheel.element.sprite.frameKeys[frameNum]')) {
        // 直接替换整行
        const oldLine = 'const frameKey = this.wheel.element.sprite.frameKeys[frameNum];';
        const newLines = `const sprite = this.wheel.element.sprite;
            if (!sprite || !sprite.frameKeys || !sprite.atlas) {
                console.warn('[AimAdjustmentBar] Sprite not ready, skipping wheel initialization');
                return;
            }
            const frameKey = sprite.frameKeys[frameNum];`;
        if (content.includes(oldLine)) {
            modified = modified.replace(oldLine, newLines);
            // 同时替换 atlas 的访问
            const oldAtlas = 'const atlas = this.wheel.element.sprite.atlas;';
            const newAtlas = 'const atlas = sprite.atlas;';
            modified = modified.replace(oldAtlas, newAtlas);
            // 添加对 atlas.frames 的空值检查
            const oldRect = 'const rect = atlas.frames[frameKey].rect;';
            const newRect = `if (!atlas.frames || !atlas.frames[frameKey]) {
                console.warn('[AimAdjustmentBar] Atlas frame not found:', frameKey);
                return;
            }
            const rect = atlas.frames[frameKey].rect;`;
            modified = modified.replace(oldRect, newRect);
            fixCount++;
            console.log(`[SourceBuilder] 修复 ${scriptName}: sprite 空值检查`);
        }
    }
    // 可以在这里添加更多已知问题的修复...
    if (fixCount > 0) {
        console.log(`[SourceBuilder] 共修复 ${scriptName} 中的 ${fixCount} 个已知问题`);
    }
    return modified;
}
/**
 * 重写 pc.app 访问为 window.pc.app
 *
 * 问题背景：
 * 在 ESM 模式下，用户脚本通过 `import * as pc from 'playcanvas'` 导入。
 * ESM 命名空间对象是静态的、冻结的，无法在运行时添加 `app` 属性。
 * 但 PlayCanvas 脚本习惯使用 `pc.app.root`、`pc.app.assets` 等方式访问应用实例。
 *
 * 解决方案：
 * 将代码中的 `pc.app` 重写为 `window.pc.app`，因为 `window.pc` 是可变的，
 * 在 index.esm.mjs 中会设置 `window.pc.app = app`。
 */
function rewritePcAppAccess(content) {
    // 匹配 pc.app 的访问模式
    // 不匹配已经是 window.pc.app 的情况
    // 使用负向后瞻确保前面不是 window. 或其他字母
    // 使用两个步骤：
    // 1. 先统计需要替换的数量
    // 2. 再执行替换
    const pcAppRegex = /(?<!window\.)(?<!\w)pc\.app\b/g;
    // 统计匹配数量（在替换前）
    const matches = content.match(pcAppRegex);
    const matchCount = matches ? matches.length : 0;
    // 如果没有需要替换的内容，直接返回
    if (matchCount === 0) {
        return content;
    }
    // 执行替换
    const modified = content.replace(pcAppRegex, 'window.pc.app');
    console.log(`[SourceBuilder] 重写了 ${matchCount} 处 pc.app -> window.pc.app`);
    return modified;
}
/**
 * 构建脚本的文件夹路径（从 asset.path 数组）
 */
function buildScriptFolderPath(pathIds, folderMap) {
    const pathNames = [];
    // 跳过根文件夹（第一个元素），构建路径
    for (let i = 1; i < pathIds.length; i++) {
        const folderId = String(pathIds[i]);
        const folderName = folderMap.get(folderId);
        if (folderName) {
            pathNames.push(folderName);
        }
    }
    return pathNames.join('/');
}
/**
 * 根据 import map 确定目标路径
 */
function resolveTargetPathFromImportMap(folderPath, filename, importMap) {
    const imports = importMap.content.imports || {};
    // 尝试匹配 import map 中的前缀
    for (const [prefix, target] of Object.entries(imports)) {
        // prefix 可能是 "Gameplay/" 这样的目录前缀
        if (prefix.endsWith('/')) {
            const prefixName = prefix.slice(0, -1); // 移除末尾的 '/'
            // 检查文件夹路径是否以该前缀开始
            if (folderPath === prefixName || folderPath.startsWith(prefixName + '/')) {
                // 计算相对于前缀的剩余路径
                const relativePath = folderPath === prefixName
                    ? ''
                    : folderPath.slice(prefixName.length + 1);
                // 构建目标路径（使用正斜杠，因为 ESM import 需要 URL 风格路径）
                const targetBase = target.startsWith('./') ? target.slice(2) : target;
                const resultPath = relativePath
                    ? path.join(targetBase, relativePath, filename)
                    : path.join(targetBase, filename);
                // 确保使用正斜杠（Windows 兼容）
                return resultPath.replace(/\\/g, '/');
            }
        }
    }
    // 如果没有匹配的前缀，使用原始路径
    const defaultPath = folderPath ? path.join(folderPath, filename) : filename;
    // 确保使用正斜杠（Windows 兼容）
    return defaultPath.replace(/\\/g, '/');
}
/**
 * 复制 Import Map 中引用的第三方库文件
 * 例如 planck.mjs 等
 */
async function copyImportMapLibraries(projectConfig, projectDir, outputDir, importMap) {
    const imports = importMap.content.imports || {};
    const assets = projectConfig.format === 'playcanvas'
        ? projectConfig.assets
        : projectConfig.assets;
    for (const [key, value] of Object.entries(imports)) {
        // 跳过目录前缀（以 / 结尾的）
        if (key.endsWith('/'))
            continue;
        // 跳过 playcanvas 引擎（由模板处理）
        if (key === 'playcanvas')
            continue;
        // 处理单个文件的导入（如 "planck": "./Lib/planck.mjs"）
        let targetPath = value;
        // 修复路径（./Lib/ -> ./）
        if (targetPath.startsWith('./Lib/')) {
            targetPath = './' + targetPath.slice(6);
        }
        // 移除 ./ 前缀
        const outputRelPath = targetPath.startsWith('./') ? targetPath.slice(2) : targetPath;
        const outputPath = path.join(outputDir, outputRelPath);
        // 检查目标文件是否已存在（可能已被 copyUserScriptsForESM 复制）
        try {
            await fs.access(outputPath);
            console.log(`[ESM] 第三方库已存在，跳过: ${key} -> ${outputRelPath}`);
            continue;
        }
        catch {
            // 文件不存在，需要复制
        }
        // 查找源文件
        // 1. 首先检查是否是 asset 引用（通过 name 查找），使用统一的解析函数
        let sourceFound = false;
        const filename = path.basename(outputRelPath);
        for (const [assetId, asset] of Object.entries(assets)) {
            const assetData = asset;
            if (assetData.type !== 'script')
                continue;
            if (assetData.name !== key && assetData.file?.filename !== filename)
                continue;
            // 使用统一的解析函数查找源文件（支持 PlayCraft Git 仓库结构）
            const sourcePath = await resolveScriptSourcePath(assetData, assetId, projectDir, assets);
            if (sourcePath) {
                try {
                    await fs.mkdir(path.dirname(outputPath), { recursive: true });
                    await fs.copyFile(sourcePath, outputPath);
                    console.log(`[ESM] 复制第三方库: ${key} -> ${outputRelPath}`);
                    sourceFound = true;
                }
                catch (error) {
                    console.warn(`[ESM] 无法复制第三方库 ${key}:`, error);
                }
                break;
            }
        }
        // 2. 如果没有在 assets 中找到，尝试在项目目录中直接查找
        if (!sourceFound) {
            // 尝试原始路径（例如 ./Lib/planck.mjs）
            const originalPath = value.startsWith('./') ? value.slice(2) : value;
            const candidates = [
                path.join(projectDir, originalPath),
                path.join(projectDir, filename),
                path.join(projectDir, 'Lib', filename),
                // PlayCraft Git 仓库结构
                path.join(projectDir, 'assets', filename),
                path.join(projectDir, 'assets', originalPath),
            ];
            for (const candidate of candidates) {
                try {
                    await fs.access(candidate);
                    await fs.mkdir(path.dirname(outputPath), { recursive: true });
                    await fs.copyFile(candidate, outputPath);
                    console.log(`[ESM] 复制第三方库（从目录）: ${key} -> ${outputRelPath}`);
                    sourceFound = true;
                    break;
                }
                catch {
                    // 继续尝试
                }
            }
        }
        if (!sourceFound) {
            console.warn(`[ESM] 未找到第三方库 ${key} 的源文件`);
        }
    }
}
