import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
import { minifyPatchCode } from '../utils/minify.js';
/**
 * PlayCanvas Vite 插件
 * 处理 PlayCanvas 特定的资源转换和内联
 */
export function vitePlayCanvasPlugin(options) {
    // 保存插件上下文，用于调用 Vite 的 load 方法
    let pluginContext = null;
    return {
        name: 'vite-plugin-playcanvas',
        enforce: 'pre',
        buildStart() {
            // 保存插件的 this 上下文
            pluginContext = this;
        },
        // 使用 transformIndexHtml 的对象形式，指定在 pre 阶段执行
        // 这确保在 Vite 处理资源引用之前移除不需要的引用
        transformIndexHtml: {
            order: 'pre',
            async handler(html) {
                if (options.ammoReplacement) {
                    html = await injectPhysicsLibrary(html, options);
                }
                // ⚠️ 在最早期移除 manifest 引用，避免 Vite 将其作为资源处理
                if (options.outputFormat === 'html' && options.convertDataUrls) {
                    html = html.replace(/<link[^>]*rel=["']manifest["'][^>]*>/gi, '');
                }
                if (options.outputFormat === 'html') {
                    // 1. 内联 PlayCanvas Engine + 引擎补丁
                    html = await inlineEngineScript(html, options.baseBuildDir, options);
                    // 2. 根据模式处理用户脚本
                    if (options.buildMode === 'esm' && options.isESMBundle) {
                        // ESM → IIFE：脚本已被 viteESMBundlePlugin 打包并注入到 HTML
                        // ESM 模式的 index.esm.mjs 已包含完整的初始化逻辑，不需要 __start__.js 等
                        console.log('[PlayCanvasPlugin] ESM Bundle 模式：脚本由 ESMBundle 插件处理');
                        // ⚠️ 重要：ESM Bundle 模式也需要将 config.json 和场景资源内联到打包后的代码中
                        // 因为打包后的 IIFE 代码中 CONFIG_FILENAME = "config.json" 是硬编码的
                        html = await inlineESMBundleAssets(html, options.baseBuildDir, options, pluginContext);
                        // 处理 CSS 和 manifest
                        html = await inlineCSS(html, options.baseBuildDir, options);
                        html = await inlineManifest(html, options.baseBuildDir, options);
                    }
                    else {
                        // Classic 模式或 ESM 原生模式
                        if (options.buildMode === 'classic' && options.inlineGameScripts) {
                            // Classic 模式：内联 __game-scripts.js
                            html = await inlineGameScripts(html, options.baseBuildDir);
                        }
                        // ESM 原生模式：保持 <script type="module"> 和 import map
                        // 3. 内联并转换 __settings__.js（传递插件上下文）
                        html = await inlineAndConvertSettings(html, options.baseBuildDir, options, pluginContext);
                        // 4. 内联 __modules__.js
                        html = await inlineModulesScript(html, options.baseBuildDir);
                        // 5. 内联 __start__.js
                        html = await inlineStartScript(html, options.baseBuildDir, options);
                        // 6. 内联 __loading__.js
                        html = await inlineLoadingScript(html, options.baseBuildDir);
                        // 7. 内联 CSS
                        html = await inlineCSS(html, options.baseBuildDir, options);
                        // 8. 处理 manifest.json
                        html = await inlineManifest(html, options.baseBuildDir, options);
                    }
                }
                else if (options.outputFormat === 'zip') {
                    // ZIP 格式：不内联引擎，但需要在 IIFE 之前添加引擎脚本标签
                    if (options.buildMode === 'esm' && options.isESMBundle) {
                        // ESM → IIFE 模式：IIFE 依赖全局 pc 变量，需要先加载引擎
                        console.log('[PlayCanvasPlugin] ZIP + ESM Bundle 模式：添加引擎脚本标签');
                        html = addEngineScriptTag(html);
                    }
                    // 对于 Classic 模式或 ESM 原生模式，保持原有的多文件结构
                }
                // 注意：HTML 压缩不能在这里执行，因为 Vite 还需要解析 HTML
                // HTML 模板已经在 templates/ 中被压缩为单行
                // 最终的 HTML 压缩由 vite-plugin-singlefile 在最后阶段处理
                return html;
            },
        },
        async transform(code, id) {
            // 转换 __settings__.js 中的资源路径为 data URLs（传递插件上下文）
            if (id.endsWith('__settings__.js')) {
                return await convertSettingsToDataUrls(code, options.baseBuildDir, options, pluginContext);
            }
            return code;
        },
    };
}
/**
 * 内联 PlayCanvas Engine
 */
async function inlineEngineScript(html, baseBuildDir, options) {
    // Classic 模式的引擎文件名
    const classicEngineNames = [
        'playcanvas-stable.min.js',
        'playcanvas.min.js',
        '__lib__.js',
    ];
    // ESM 模式的引擎文件位置
    const esmEngineNames = [
        'js/playcanvas-engine-umd.js',
    ];
    // 根据构建模式选择引擎文件列表
    const engineNames = options.buildMode === 'esm' && options.isESMBundle
        ? [...esmEngineNames, ...classicEngineNames] // ESM 优先，然后 Classic
        : classicEngineNames;
    for (const engineName of engineNames) {
        const enginePath = path.join(baseBuildDir, engineName);
        try {
            await fs.access(enginePath);
            let engineCode = await fs.readFile(enginePath, 'utf-8');
            const patchScripts = await getEnginePatchScripts(options);
            const engineScript = options.compressEngine
                ? `${await getLz4InlineScript()}${await buildCompressedEngineScript(engineCode)}`
                : `<script>${engineCode}</script>`;
            // 对于 ESM 模式，引擎需要在 </head> 之前插入（确保在用户脚本之前加载）
            if (options.buildMode === 'esm' && options.isESMBundle) {
                // ESM 模式：在 </head> 之前插入引擎脚本
                // 这确保 window.pc 在 IIFE 用户脚本执行前已设置
                html = html.replace('</head>', `${engineScript}${patchScripts}\n</head>`);
                console.log(`[PlayCanvasPlugin] ESM 模式：已内联引擎 ${engineName}`);
                return html;
            }
            // Classic 模式：替换原有的 script 标签
            const scriptPattern = new RegExp(`<script[^>]*src=["']${engineName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["'][^>]*></script>`, 'i');
            html = html.replace(scriptPattern, `${engineScript}${patchScripts}`);
            return html;
        }
        catch (error) {
            // 继续尝试下一个
        }
    }
    return html;
}
/**
 * 为 ZIP 格式添加引擎脚本标签（不内联）
 * 用于 ESM → IIFE 模式，IIFE 依赖全局 pc 变量
 */
function addEngineScriptTag(html) {
    // 在 </head> 之前添加引擎脚本标签
    // UMD 版本会设置 window.pc 全局变量
    const engineScriptTag = `<script src="./js/playcanvas-engine-umd.js"></script>`;
    html = html.replace('</head>', `${engineScriptTag}\n</head>`);
    console.log('[PlayCanvasPlugin] ZIP 模式：已添加引擎脚本标签');
    return html;
}
/**
 * 内联并转换 __settings__.js
 */
async function inlineAndConvertSettings(html, baseBuildDir, options, pluginContext) {
    const settingsPath = path.join(baseBuildDir, '__settings__.js');
    try {
        await fs.access(settingsPath);
    }
    catch (error) {
        // __settings__.js 不存在，尝试从 config.json 生成
        return await generateAndInlineSettings(html, baseBuildDir, options, pluginContext);
    }
    let settingsCode = await fs.readFile(settingsPath, 'utf-8');
    // 转换资源URL为data URLs（传递插件上下文）
    settingsCode = await convertSettingsToDataUrls(settingsCode, baseBuildDir, options, pluginContext);
    // 替换 script 标签
    const scriptPattern = /<script[^>]*src=["']__settings__\.js["'][^>]*><\/script>/i;
    html = html.replace(scriptPattern, `<script>${settingsCode}</script>`);
    return html;
}
/**
 * 生成并内联 settings（如果 __settings__.js 不存在）
 */
async function generateAndInlineSettings(html, baseBuildDir, options, pluginContext) {
    const configPath = path.join(baseBuildDir, 'config.json');
    const configContent = await fs.readFile(configPath, 'utf-8');
    let configJson = JSON.parse(configContent);
    if (options.ammoReplacement) {
        configJson = stripAmmoAssets(configJson);
    }
    if (options.convertDataUrls) {
        configJson = await inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext);
    }
    if (options.mraidSupport) {
        configJson = applyMraidConfig(configJson);
    }
    // 生成 config 值
    const configValue = buildConfigValue(configJson);
    // 生成 scene data URL
    let sceneDataUrl = '';
    if (configJson.scenes && configJson.scenes.length > 0) {
        const sceneUrl = configJson.scenes[0].url;
        if (sceneUrl && !sceneUrl.startsWith('data:')) {
            const scenePath = path.join(baseBuildDir, sceneUrl);
            try {
                const sceneContent = await fs.readFile(scenePath, 'utf-8');
                sceneDataUrl = `data:application/json;base64,${Buffer.from(sceneContent).toString('base64')}`;
            }
            catch (error) {
                console.warn(`警告: 场景文件不存在: ${sceneUrl}`);
            }
        }
        else {
            sceneDataUrl = sceneUrl;
        }
    }
    const appProps = configJson.application_properties || {};
    const scripts = appProps.scripts || [];
    const preloadModules = extractPreloadModules(configJson);
    // 保留原始的 powerPreference 设置，默认为 high-performance
    const powerPreference = appProps.powerPreference || 'high-performance';
    const settingsCode = `
window.ASSET_PREFIX = "";
window.SCRIPT_PREFIX = "";
window.SCENE_PATH = "${sceneDataUrl}";
window.CONTEXT_OPTIONS = {
    'antialias': ${appProps.antiAlias !== false},
    'alpha': ${appProps.transparentCanvas === true},
    'preserveDrawingBuffer': ${appProps.preserveDrawingBuffer === true},
    'deviceTypes': ['webgl2', 'webgl1'],
    'powerPreference': "${powerPreference}"
};
window.SCRIPTS = [${scripts.join(', ')}];
window.CONFIG_FILENAME = ${configValue};
window.INPUT_SETTINGS = {
    useKeyboard: ${appProps.useKeyboard !== false},
    useMouse: ${appProps.useMouse !== false},
    useGamepads: ${appProps.useGamepads === true},
    useTouch: ${appProps.useTouch !== false}
};
pc.script.legacy = ${appProps.useLegacyScripts === true};
window.PRELOAD_MODULES = ${JSON.stringify(preloadModules)};
`;
    // 在 </head> 之前插入
    html = html.replace('</head>', `<script>${settingsCode}</script>\n</head>`);
    return html;
}
/**
 * ESM Bundle 模式专用：将 config.json 和场景资源内联到打包后的 IIFE 代码中
 *
 * 在 ESM Bundle 模式下，index.esm.mjs 已经被打包为 IIFE 并注入到 HTML 的 <script> 标签中
 * 该 IIFE 代码中 CONFIG_FILENAME = "config.json" 是硬编码的字符串
 * 此函数需要将其替换为内联的 data URL，使得单文件 HTML 可以正常工作
 */
async function inlineESMBundleAssets(html, baseBuildDir, options, pluginContext) {
    console.log('[PlayCanvasPlugin] ESM Bundle 模式：开始内联 config.json 和场景资源...');
    // 1. 读取并处理 config.json
    const configPath = path.join(baseBuildDir, 'config.json');
    let configJson;
    try {
        const configContent = await fs.readFile(configPath, 'utf-8');
        configJson = JSON.parse(configContent);
    }
    catch (error) {
        console.warn(`[PlayCanvasPlugin] ESM Bundle: 无法读取 config.json: ${error}`);
        return html;
    }
    // 2. 处理 Ammo 物理引擎替换
    if (options.ammoReplacement) {
        configJson = stripAmmoAssets(configJson);
    }
    // 3. 内联 config.json 中的所有资源 URL
    if (options.convertDataUrls) {
        configJson = await inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext);
    }
    // 4. 应用 MRAID 配置
    if (options.mraidSupport) {
        configJson = applyMraidConfig(configJson);
    }
    // 5. 生成 config.json 的 data URL
    // 如果启用 compressConfigJson，使用 LZ4 压缩后再 base64 编码
    const configJsonString = JSON.stringify(configJson);
    let configDataUrl;
    if (options.compressConfigJson) {
        const lz4 = await loadLz4Module();
        const compressed = lz4.compress(Buffer.from(configJsonString));
        const compressedBase64 = Buffer.from(compressed).toString('base64');
        // 使用 lz4-json 前缀标识压缩格式，运行时需要解压
        configDataUrl = `data:application/x-lz4-json;base64,${compressedBase64}`;
        const originalSize = Buffer.from(configJsonString).length;
        const compressedSize = compressed.length;
        const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(1);
        console.log(`[PlayCanvasPlugin] ESM Bundle: config.json 已压缩 (${formatBytes(originalSize)} → ${formatBytes(compressedSize)}, 减少 ${ratio}%)`);
    }
    else {
        configDataUrl = `data:application/json;base64,${Buffer.from(configJsonString).toString('base64')}`;
    }
    // 6. 处理场景文件 - 获取第一个场景的 data URL
    let sceneDataUrl = '';
    if (configJson.scenes && configJson.scenes.length > 0) {
        const sceneUrl = configJson.scenes[0].url;
        if (sceneUrl && !sceneUrl.startsWith('data:')) {
            const scenePath = path.join(baseBuildDir, sceneUrl);
            try {
                const sceneContent = await fs.readFile(scenePath, 'utf-8');
                sceneDataUrl = `data:application/json;base64,${Buffer.from(sceneContent).toString('base64')}`;
                console.log(`[PlayCanvasPlugin] ESM Bundle: 场景已内联: ${sceneUrl}`);
            }
            catch (error) {
                console.warn(`[PlayCanvasPlugin] ESM Bundle: 场景文件不存在: ${sceneUrl}`);
            }
        }
        else {
            sceneDataUrl = sceneUrl || '';
        }
    }
    // 7. (Logo removed - no longer needed)
    // 8. 在 HTML 中查找并替换 ESM Bundle IIFE 代码中的配置值
    // 方案1+3：直接将 CONFIG_FILENAME = "config.json" 替换为 data URL
    // 8.1 替换 CONFIG_FILENAME
    // 格式1: CONFIG_FILENAME = "config.json"
    // 格式2: CONFIG_FILENAME="config.json"  (压缩后)
    const configReplaceCount = (html.match(/CONFIG_FILENAME\s*=\s*["']config\.json["']/g) || []).length;
    if (configReplaceCount > 0) {
        html = html.replace(/CONFIG_FILENAME\s*=\s*["']config\.json["']/g, `CONFIG_FILENAME="${configDataUrl}"`);
        console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${configReplaceCount} 处 CONFIG_FILENAME`);
    }
    // 8.2 替换 configure("config.json") 调用
    // Vite 压缩后可能是: .configure("config.json",
    const configureReplaceCount = (html.match(/\.configure\s*\(\s*["']config\.json["']/g) || []).length;
    if (configureReplaceCount > 0) {
        html = html.replace(/\.configure\s*\(\s*["']config\.json["']/g, `.configure("${configDataUrl}"`);
        console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${configureReplaceCount} 处 .configure("config.json")`);
    }
    // 9. 替换 SCENE_PATH
    if (sceneDataUrl) {
        const escapedSceneDataUrl = sceneDataUrl.replace(/\$/g, '$$$$');
        // 查找 SCENE_PATH 的各种格式并替换
        // 格式: const SCENE_PATH = "xxx.json" 或 SCENE_PATH = "xxx"
        html = html.replace(/SCENE_PATH\s*=\s*["'][^"']+\.json["']/g, `SCENE_PATH = "${escapedSceneDataUrl}"`);
        // ⚠️ Vite 压缩后格式：loadScene("xxx.json" 或 loadScene(变量名,
        // 需要同时替换 loadScene 调用中的场景路径字符串
        // 格式1: .loadScene("2412781.json", ...)
        // 格式2: .loadScene(变量名, ...) - 需要替换变量定义
        const sceneUrl = configJson.scenes?.[0]?.url;
        if (sceneUrl && !sceneUrl.startsWith('data:')) {
            // 直接替换 loadScene 中的场景路径字符串
            const scenePathRegex = new RegExp(`\\.loadScene\\s*\\(\\s*["']${sceneUrl.replace(/\./g, '\\.')}["']`, 'g');
            const loadSceneReplaceCount = (html.match(scenePathRegex) || []).length;
            if (loadSceneReplaceCount > 0) {
                html = html.replace(scenePathRegex, `.loadScene("${escapedSceneDataUrl}"`);
                console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${loadSceneReplaceCount} 处 .loadScene("${sceneUrl}") 调用`);
            }
            // 同时替换压缩后的变量定义：const vh="2412781.json" 或 变量名="2412781.json"
            // 这会处理如 const vh="2412781.json" 这种情况
            const varDefRegex = new RegExp(`(\\w{1,3})\\s*=\\s*["']${sceneUrl.replace(/\./g, '\\.')}["']`, 'g');
            const varDefReplaceCount = (html.match(varDefRegex) || []).length;
            if (varDefReplaceCount > 0) {
                html = html.replace(varDefRegex, `$1="${escapedSceneDataUrl}"`);
                console.log(`[PlayCanvasPlugin] ESM Bundle: 替换了 ${varDefReplaceCount} 处场景路径变量定义`);
            }
        }
    }
    // 10. 处理 PRELOAD_MODULES（WASM 模块）
    // 在 ESM Bundle 模式下，需要将 PRELOAD_MODULES 中的 URL 也转换为 data URL
    const modulesRegex = /PRELOAD_MODULES\s*=\s*(\[[\s\S]*?\]);/;
    const modulesMatch = html.match(modulesRegex);
    if (modulesMatch) {
        try {
            const modulesStr = modulesMatch[1];
            let modules = new Function(`return ${modulesStr}`)();
            // 过滤 Ammo 模块
            if (options.ammoReplacement) {
                modules = modules.filter((module) => !isAmmoModule(module));
            }
            // 转换模块 URL 为 data URL
            for (const module of modules) {
                if (module.fallbackUrl && !module.fallbackUrl.startsWith('data:')) {
                    const fallbackPath = path.join(baseBuildDir, module.fallbackUrl);
                    try {
                        const fallbackCode = await fs.readFile(fallbackPath, 'utf-8');
                        module.fallbackUrl = `data:text/javascript;base64,${Buffer.from(fallbackCode).toString('base64')}`;
                        // 清空 WASM URL，强制使用 fallback
                        module.glueUrl = '';
                        module.wasmUrl = '';
                    }
                    catch (error) {
                        console.warn(`[PlayCanvasPlugin] ESM Bundle: 无法读取 fallback: ${module.fallbackUrl}`);
                    }
                }
            }
            // 替换 PRELOAD_MODULES
            const newModulesStr = JSON.stringify(modules);
            const escapedModulesStr = newModulesStr.replace(/\$/g, '$$$$');
            html = html.replace(modulesRegex, `PRELOAD_MODULES = ${escapedModulesStr};`);
        }
        catch (error) {
            console.warn(`[PlayCanvasPlugin] ESM Bundle: 无法处理 PRELOAD_MODULES: ${error}`);
        }
    }
    // 11. 注入 ScriptHandler 补丁（参考 PlayCanvas 官方 one-page-inline-game-scripts.js）
    // 这个补丁覆盖 pc.ScriptHandler.prototype._loadScript，使其能够：
    // 1. 正确处理 data URL 格式的脚本
    // 2. 对于空脚本（已打包到 IIFE 中的），直接跳过执行
    const scriptHandlerPatchCode = `
(function() {
    if (typeof pc === 'undefined' || !pc.ScriptHandler) return;
    
    var originalLoadScript = pc.ScriptHandler.prototype._loadScript;
    pc.ScriptHandler.prototype._loadScript = function(url, callback) {
        if (url && url.startsWith('data:text/javascript;base64,')) {
            var head = document.head;
            var element = document.createElement('script');
            this._cache[url] = element;
            element.async = false;
            
            var index = url.indexOf(',');
            var base64 = url.slice(index + 1);
            
            if (!base64 || base64.length === 0) {
                callback(null, url, element);
                return;
            }
            
            try {
                var data = window.atob(base64);
                element.innerText = data;
                head.appendChild(element);
            } catch (e) {
                console.warn('[ScriptHandler] Failed to decode script:', e);
            }
            
            callback(null, url, element);
            return;
        }
        
        return originalLoadScript.call(this, url, callback);
    };
})();`;
    const minifiedScriptHandlerPatch = await minifyPatchCode(scriptHandlerPatchCode, 'scriptHandlerPatch');
    // 将补丁注入到 </head> 之前，确保在引擎之后、脚本加载之前执行
    if (html.includes('</head>')) {
        html = html.replace('</head>', `<script>${minifiedScriptHandlerPatch}</script>\n</head>`);
    }
    console.log('[PlayCanvasPlugin] ESM Bundle 模式：资源内联完成');
    return html;
}
/**
 * 转换 settings 代码中的资源URL为 data URLs
 */
async function convertSettingsToDataUrls(settingsCode, baseBuildDir, options, pluginContext) {
    // 1. 转换 config.json（传递插件上下文）
    settingsCode = await convertConfigUrl(settingsCode, baseBuildDir, options, pluginContext);
    // 2. 转换场景文件
    settingsCode = await convertSceneUrl(settingsCode, baseBuildDir);
    // 3. 转换 PRELOAD_MODULES（优先使用 JS fallback）
    settingsCode = await convertPreloadModules(settingsCode, baseBuildDir, options);
    return settingsCode;
}
/**
 * 转换 CONFIG_FILENAME 为 data URL
 */
async function convertConfigUrl(settingsCode, baseBuildDir, options, pluginContext) {
    const configMatch = settingsCode.match(/window\.CONFIG_FILENAME\s*=\s*"([^"]+)"/);
    if (!configMatch) {
        return settingsCode;
    }
    const configPath = configMatch[1];
    if (configPath.startsWith('data:')) {
        return settingsCode; // 已经是 data URL
    }
    const fullConfigPath = path.join(baseBuildDir, configPath);
    try {
        const configContent = await fs.readFile(fullConfigPath, 'utf-8');
        let configJson = JSON.parse(configContent);
        if (options.ammoReplacement) {
            configJson = stripAmmoAssets(configJson);
        }
        if (options.convertDataUrls) {
            configJson = await inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext);
        }
        if (options.mraidSupport) {
            configJson = applyMraidConfig(configJson);
        }
        const configValue = buildConfigValue(configJson);
        return settingsCode.replace(/window\.CONFIG_FILENAME\s*=\s*"[^"]+"/, `window.CONFIG_FILENAME = ${configValue}`);
    }
    catch (error) {
        console.warn(`警告: 无法读取配置文件: ${configPath}`);
        return settingsCode;
    }
}
/**
 * 转换 SCENE_PATH 为 data URL
 */
async function convertSceneUrl(settingsCode, baseBuildDir) {
    const sceneMatch = settingsCode.match(/window\.SCENE_PATH\s*=\s*"([^"]+)"/);
    if (!sceneMatch) {
        return settingsCode;
    }
    const scenePath = sceneMatch[1];
    if (scenePath.startsWith('data:') || !scenePath) {
        return settingsCode; // 已经是 data URL 或为空
    }
    const fullScenePath = path.join(baseBuildDir, scenePath);
    try {
        const sceneContent = await fs.readFile(fullScenePath, 'utf-8');
        const sceneDataUrl = `data:application/json;base64,${Buffer.from(sceneContent).toString('base64')}`;
        return settingsCode.replace(/window\.SCENE_PATH\s*=\s*"[^"]+"/, `window.SCENE_PATH = "${sceneDataUrl}"`);
    }
    catch (error) {
        console.warn(`警告: 无法读取场景文件: ${scenePath}`);
        return settingsCode;
    }
}
/**
 * 转换 PRELOAD_MODULES 中的资源URL
 * 对于 Playable Ads，优先使用 fallback JS 版本（跳过 WASM）
 */
async function convertPreloadModules(settingsCode, baseBuildDir, options) {
    // 匹配 PRELOAD_MODULES 数组（支持单引号和双引号）
    const modulesMatch = settingsCode.match(/window\.PRELOAD_MODULES\s*=\s*(\[[\s\S]*?\]);/);
    if (!modulesMatch) {
        return settingsCode;
    }
    try {
        // 使用 Function 构造函数安全地解析 JavaScript 对象字面量
        const modulesStr = modulesMatch[1];
        let modules = new Function(`return ${modulesStr}`)();
        if (options.ammoReplacement) {
            const beforeCount = modules.length;
            modules = modules.filter((module) => !isAmmoModule(module));
            const removed = beforeCount - modules.length;
            if (removed > 0) {
                console.warn(`警告: 已移除 ${removed} 个 Ammo 预加载模块，替换为 ${options.ammoReplacement}`);
            }
        }
        // 转换每个模块的URL为data URL
        for (const module of modules) {
            // 对于 Playable Ads，优先使用 fallback JS 版本，跳过 WASM
            if (module.fallbackUrl && !module.fallbackUrl.startsWith('data:')) {
                const fallbackPath = path.join(baseBuildDir, module.fallbackUrl);
                try {
                    const fallbackCode = await fs.readFile(fallbackPath, 'utf-8');
                    module.fallbackUrl = `data:text/javascript;base64,${Buffer.from(fallbackCode).toString('base64')}`;
                    // 清空 WASM 相关 URL，强制使用 fallback
                    module.glueUrl = '';
                    module.wasmUrl = '';
                }
                catch (error) {
                    console.warn(`警告: 无法读取 fallback 文件: ${module.fallbackUrl}`);
                    // 如果 fallback 读取失败，尝试读取 WASM 相关文件
                    if (module.glueUrl && !module.glueUrl.startsWith('data:')) {
                        const gluePath = path.join(baseBuildDir, module.glueUrl);
                        try {
                            const glueCode = await fs.readFile(gluePath, 'utf-8');
                            module.glueUrl = `data:text/javascript;base64,${Buffer.from(glueCode).toString('base64')}`;
                        }
                        catch (error) {
                            console.warn(`警告: 无法读取 glue 文件: ${module.glueUrl}`);
                        }
                    }
                    if (module.wasmUrl && !module.wasmUrl.startsWith('data:')) {
                        const wasmPath = path.join(baseBuildDir, module.wasmUrl);
                        try {
                            const wasmBinary = await fs.readFile(wasmPath);
                            module.wasmUrl = `data:application/wasm;base64,${wasmBinary.toString('base64')}`;
                        }
                        catch (error) {
                            console.warn(`警告: 无法读取 WASM 文件: ${module.wasmUrl}`);
                        }
                    }
                }
            }
            else {
                // 如果没有 fallback，尝试转换 WASM 相关文件
                if (module.glueUrl && !module.glueUrl.startsWith('data:')) {
                    const gluePath = path.join(baseBuildDir, module.glueUrl);
                    try {
                        const glueCode = await fs.readFile(gluePath, 'utf-8');
                        module.glueUrl = `data:text/javascript;base64,${Buffer.from(glueCode).toString('base64')}`;
                    }
                    catch (error) {
                        console.warn(`警告: 无法读取 glue 文件: ${module.glueUrl}`);
                    }
                }
                if (module.wasmUrl && !module.wasmUrl.startsWith('data:')) {
                    const wasmPath = path.join(baseBuildDir, module.wasmUrl);
                    try {
                        const wasmBinary = await fs.readFile(wasmPath);
                        module.wasmUrl = `data:application/wasm;base64,${wasmBinary.toString('base64')}`;
                    }
                    catch (error) {
                        console.warn(`警告: 无法读取 WASM 文件: ${module.wasmUrl}`);
                    }
                }
            }
        }
        // 重新生成 PRELOAD_MODULES 配置（使用双引号 JSON 格式）
        const newModulesStr = JSON.stringify(modules, null, 4);
        return settingsCode.replace(/window\.PRELOAD_MODULES\s*=\s*\[[\s\S]*?\];/, `window.PRELOAD_MODULES = ${newModulesStr};`);
    }
    catch (error) {
        console.warn(`警告: 无法解析 PRELOAD_MODULES: ${error instanceof Error ? error.message : String(error)}`);
        return settingsCode;
    }
}
/**
 * 从 config.json 提取 PRELOAD_MODULES
 */
function extractPreloadModules(configJson) {
    const modules = [];
    if (configJson.assets) {
        for (const [id, asset] of Object.entries(configJson.assets)) {
            const assetData = asset;
            if (assetData.type === 'wasm' && assetData.data) {
                const moduleData = assetData.data;
                const fallbackAssetId = moduleData.fallbackScriptId;
                let fallbackUrl = '';
                if (fallbackAssetId && configJson.assets[fallbackAssetId]) {
                    const fallbackAsset = configJson.assets[fallbackAssetId];
                    fallbackUrl = fallbackAsset.file?.url || '';
                }
                if (moduleData.moduleName && fallbackUrl) {
                    modules.push({
                        moduleName: moduleData.moduleName,
                        glueUrl: '',
                        wasmUrl: '',
                        fallbackUrl: fallbackUrl,
                        preload: true,
                    });
                }
            }
        }
    }
    return modules;
}
/**
 * 内联 __modules__.js
 */
async function inlineModulesScript(html, baseBuildDir) {
    const modulesPath = path.join(baseBuildDir, '__modules__.js');
    try {
        await fs.access(modulesPath);
    }
    catch (error) {
        // __modules__.js 不存在，跳过
        return html;
    }
    const modulesCode = await fs.readFile(modulesPath, 'utf-8');
    // 替换 script 标签
    const scriptPattern = /<script[^>]*src=["']__modules__\.js["'][^>]*><\/script>/i;
    html = html.replace(scriptPattern, `<script>${modulesCode}</script>`);
    // 如果 HTML 中没有找到 script 标签，在 <body> 开头添加
    if (!scriptPattern.test(html)) {
        html = html.replace('<body>', `<body>\n<script>${modulesCode}</script>\n`);
    }
    return html;
}
/**
 * 内联 __game-scripts.js
 * 注意：脚本需要在 pc.AppBase 创建后执行，所以插入到 </body> 之前
 * 同时包装成一个函数，在 DOMContentLoaded 后执行，确保 PlayCanvas 引擎已完全初始化
 */
/**
 * 注入打包后的游戏脚本（ESM → IIFE 场景）
 */
async function injectBundledGameScripts(html, baseBuildDir) {
    const gameScriptsPath = path.join(baseBuildDir, '__game-scripts.js');
    try {
        await fs.access(gameScriptsPath);
        const gameScriptsCode = await fs.readFile(gameScriptsPath, 'utf-8');
        // 在 PlayCanvas Engine 之后、__start__.js 之前插入
        if (html.includes('</head>')) {
            html = html.replace('</head>', `<script>${gameScriptsCode}</script>\n</head>`);
        }
    }
    catch (error) {
        console.warn('[PlayCanvasPlugin] 未找到打包后的游戏脚本');
    }
    return html;
}
async function inlineGameScripts(html, baseBuildDir) {
    const gameScriptsPath = path.join(baseBuildDir, '__game-scripts.js');
    try {
        await fs.access(gameScriptsPath);
    }
    catch (error) {
        // __game-scripts.js 不存在，跳过
        return html;
    }
    const gameScriptsCode = await fs.readFile(gameScriptsPath, 'utf-8');
    // 将脚本包装，确保在 pc.script 初始化后执行
    // pc.createScript 需要 pc.script._scripts 已初始化
    const wrappedCode = `
<script>
(function() {
  // 等待 pc.script 准备好
  function initGameScripts() {
    if (typeof pc === 'undefined' || !pc.script) {
      setTimeout(initGameScripts, 10);
      return;
    }
    ${gameScriptsCode}
  }
  // 延迟执行，确保引擎初始化完成
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initGameScripts);
  } else {
    setTimeout(initGameScripts, 0);
  }
})();
</script>`;
    // 在 </body> 之前插入，确保在其他脚本之后执行
    if (html.includes('</body>')) {
        html = html.replace('</body>', `${wrappedCode}\n</body>`);
    }
    else if (html.includes('</html>')) {
        html = html.replace('</html>', `${wrappedCode}\n</html>`);
    }
    else {
        html += wrappedCode;
    }
    return html;
}
/**
 * 内联 __start__.js
 */
async function inlineStartScript(html, baseBuildDir, options) {
    const startPath = path.join(baseBuildDir, '__start__.js');
    let startCode = await fs.readFile(startPath, 'utf-8');
    // 替换 script 标签
    const scriptPattern = /<script[^>]*src=["']__start__\.js["'][^>]*><\/script>/i;
    if (scriptPattern.test(html)) {
        html = html.replace(scriptPattern, `<script>${startCode}</script>`);
    }
    else {
        // 如果 HTML 中没有找到 script 标签，在 </body> 之前插入
        html = html.replace('</body>', `<script>${startCode}</script>\n</body>`);
    }
    return html;
}
/**
 * 内联 __loading__.js
 */
async function inlineLoadingScript(html, baseBuildDir) {
    const loadingPath = path.join(baseBuildDir, '__loading__.js');
    const scriptPattern = /<script[^>]*src=["']__loading__\.js["'][^>]*><\/script>/i;
    try {
        await fs.access(loadingPath);
        // 文件存在，内联它
        let loadingCode = await fs.readFile(loadingPath, 'utf-8');
        if (scriptPattern.test(html)) {
            html = html.replace(scriptPattern, `<script>${loadingCode}</script>`);
        }
        else {
            // 如果 HTML 中没有找到 script 标签，在 </body> 之前添加
            html = html.replace('</body>', `<script>${loadingCode}</script>\n</body>`);
        }
    }
    catch (error) {
        // __loading__.js 不存在，移除 script 标签
        if (scriptPattern.test(html)) {
            html = html.replace(scriptPattern, '');
        }
    }
    return html;
}
/**
 * 压缩 CSS 代码
 * 移除注释、多余空白、合并为单行
 */
function minifyCSS(css) {
    return css
        // 移除多行注释 /* ... */
        .replace(/\/\*[\s\S]*?\*\//g, '')
        // 移除单行注释 // ... (CSS 不标准但有些预处理器会保留)
        .replace(/\/\/.*$/gm, '')
        // 将多个空白字符（空格、换行、制表符）替换为单个空格
        .replace(/\s+/g, ' ')
        // 移除 { 前的空格
        .replace(/\s*\{\s*/g, '{')
        // 移除 } 后的空格
        .replace(/\s*\}\s*/g, '}')
        // 移除 : 前后的空格
        .replace(/\s*:\s*/g, ':')
        // 移除 ; 前后的空格
        .replace(/\s*;\s*/g, ';')
        // 移除 , 后的空格
        .replace(/,\s*/g, ',')
        // 移除最后一个 ; 在 } 前
        .replace(/;}/g, '}')
        // 去除首尾空白
        .trim();
}
/**
 * 内联 CSS 文件
 */
async function inlineCSS(html, baseBuildDir, options) {
    // 匹配 <link rel="stylesheet" href="...">
    const cssPattern = /<link[^>]*rel=["']stylesheet["'][^>]*href=["']([^"']+)["'][^>]*>/gi;
    const matches = Array.from(html.matchAll(cssPattern));
    for (const match of matches) {
        const cssPath = match[1];
        if (cssPath.startsWith('data:') || cssPath.startsWith('http://') || cssPath.startsWith('https://')) {
            continue; // 跳过已经是 data URL 或外部 URL
        }
        const fullCssPath = path.join(baseBuildDir, cssPath);
        try {
            await fs.access(fullCssPath);
            let cssContent = await fs.readFile(fullCssPath, 'utf-8');
            if (options.mraidSupport && !cssContent.includes('fill-mode-NONE')) {
                cssContent += '\n#application-canvas.fill-mode-NONE { margin: 0; width: 100%; height: 100%; }\n';
            }
            // 压缩 CSS（移除注释、多余空白、合并为单行）
            cssContent = minifyCSS(cssContent);
            // 替换 link 标签为 style 标签
            html = html.replace(match[0], `<style>${cssContent}</style>`);
        }
        catch (error) {
            console.warn(`警告: CSS 文件不存在: ${cssPath}，移除引用`);
            // 移除不存在的 CSS 引用
            html = html.replace(match[0], '');
        }
    }
    return html;
}
/**
 * 内联 manifest.json
 */
async function inlineManifest(html, baseBuildDir, options) {
    // 匹配 <link rel="manifest" href="...">
    const manifestPattern = /<link[^>]*rel=["']manifest["'][^>]*href=["']([^"']+)["'][^>]*>/gi;
    const matches = Array.from(html.matchAll(manifestPattern));
    for (const match of matches) {
        const manifestPath = match[1];
        if (options.convertDataUrls) {
            // 单文件输出不需要 manifest，移除引用避免额外产物
            html = html.replace(match[0], '');
            continue;
        }
        if (manifestPath.startsWith('data:') || manifestPath.startsWith('http://') || manifestPath.startsWith('https://')) {
            continue; // 跳过已经是 data URL 或外部 URL
        }
        const fullManifestPath = path.join(baseBuildDir, manifestPath);
        try {
            await fs.access(fullManifestPath);
            const manifestContent = await fs.readFile(fullManifestPath, 'utf-8');
            // 将 manifest 转换为 data URL 并内联到 meta 标签
            const manifestDataUrl = `data:application/manifest+json;base64,${Buffer.from(manifestContent).toString('base64')}`;
            html = html.replace(match[0], `<link rel="manifest" href="${manifestDataUrl}">`);
        }
        catch (error) {
            console.warn(`警告: manifest 文件不存在: ${manifestPath}，移除引用`);
            // 移除不存在的 manifest 引用
            html = html.replace(match[0], '');
        }
    }
    return html;
}
async function injectPhysicsLibrary(html, options) {
    const library = options.ammoReplacement;
    if (!library) {
        return html;
    }
    let scriptsToInject = [];
    if (library === 'p2') {
        // Inject p2.js (2D physics)
        const p2Code = await readPatchFile('p2.min.js');
        if (p2Code) {
            scriptsToInject.push(`<script>${p2Code}</script>`);
        }
    }
    else if (library === 'cannon') {
        // Inject Cannon.js (3D physics) + adapter
        // Get __dirname equivalent in ES modules
        const __filename = fileURLToPath(import.meta.url);
        const __dirname = path.dirname(__filename);
        const physicsDir = path.join(__dirname, '../../physics');
        try {
            // 1. Cannon.js engine
            const cannonPath = path.join(physicsDir, 'cannon-es-bundle.js');
            const cannonCode = await fs.readFile(cannonPath, 'utf-8');
            scriptsToInject.push(`<script>${cannonCode}</script>`);
            // 2. Rigidbody adapter (compatibility layer)
            const adapterPath = path.join(physicsDir, 'cannon-rigidbody-adapter.js');
            const adapterCode = await fs.readFile(adapterPath, 'utf-8');
            scriptsToInject.push(`<script>${adapterCode}</script>`);
            console.log('[PlayCanvas Plugin] Injected Cannon.js physics engine with rigidbody adapter');
        }
        catch (error) {
            console.warn('[PlayCanvas Plugin] Failed to load Cannon.js files:', error);
            return html;
        }
    }
    if (scriptsToInject.length === 0) {
        return html;
    }
    const injectedScripts = scriptsToInject.join('\n');
    if (html.includes('</head>')) {
        return html.replace('</head>', `${injectedScripts}\n</head>`);
    }
    return `${injectedScripts}\n${html}`;
}
function isAmmoModule(module) {
    const moduleName = module.moduleName?.toLowerCase() ?? '';
    const glueUrl = module.glueUrl?.toLowerCase() ?? '';
    const wasmUrl = module.wasmUrl?.toLowerCase() ?? '';
    const fallbackUrl = module.fallbackUrl?.toLowerCase() ?? '';
    return (moduleName.includes('ammo') ||
        glueUrl.includes('ammo') ||
        wasmUrl.includes('ammo') ||
        fallbackUrl.includes('ammo'));
}
async function inlineConfigAssetUrls(configJson, baseBuildDir, pluginContext) {
    if (!configJson?.assets) {
        return configJson;
    }
    const assets = configJson.assets;
    const skippedAssets = [];
    const optimizedAssets = [];
    const skippedScripts = [];
    const missingAssets = []; // 新增：缺失资源列表
    const SIZE_LIMIT = 1 * 1024 * 1024; // 1MB - 跳过超过这个大小的文件
    for (const [assetId, asset] of Object.entries(assets)) {
        const file = asset?.file;
        if (!file?.url || typeof file.url !== 'string') {
            continue;
        }
        const url = file.url;
        if (url.startsWith('data:') || url.startsWith('http://') || url.startsWith('https://')) {
            continue;
        }
        const cleanUrl = url.split('?')[0];
        const fileName = cleanUrl.toLowerCase();
        // ⚠️ 脚本资源特殊处理（参考 PlayCanvas 官方 one-page.js 实现）
        // ESM Bundle 模式下，脚本代码已被打包到 IIFE 中执行
        // PlayCanvas 官方做法：将脚本内容设为空，配合引擎补丁跳过执行
        if (cleanUrl === '__game-scripts.js') {
            // __game-scripts.js 是 Classic 模式的打包产物
            skippedScripts.push(asset.name || assetId);
            // 使用空内容的 data URL（PlayCanvas 官方做法）
            file.url = 'data:text/javascript;base64,';
            file.hash = '';
            asset.preload = false;
            continue;
        }
        if (asset.type === 'script') {
            skippedScripts.push(asset.name || assetId);
            // ⚠️ ESM 脚本特殊处理（参考 PlayCanvas 官方 one-page.js）：
            // 官方工具对于 loadingType !== 0 的脚本，会将内容设为空字符串
            // 然后配合 one-page-inline-game-scripts.js 引擎补丁处理
            // 
            // 我们的方案：
            // 1. 将脚本 URL 设为空内容的 data URL
            // 2. 清空 hash 避免追加查询参数
            // 3. 脚本代码已经通过 IIFE 中的 pc.createScript() 注册
            // 4. 需要配合引擎补丁让 ScriptHandler 跳过空脚本
            file.url = 'data:text/javascript;base64,'; // 空 JavaScript
            file.hash = ''; // 清空 hash
            asset.preload = false; // 禁用预加载
            continue;
        }
        // 跳过物理引擎缓存文件和大型文本文件
        if (fileName.endsWith('.pma.txt') ||
            fileName.includes('browsermetrics') ||
            fileName.includes('deferredbrowsermetrics') ||
            (fileName.endsWith('.txt') && file.size && file.size > SIZE_LIMIT)) {
            skippedAssets.push(`${asset.name || assetId} (${(file.size / 1024 / 1024).toFixed(2)}MB)`);
            // 标记为不预加载，避免引擎尝试加载
            asset.preload = false;
            // 提供有效的占位符数据，避免解析错误
            file.url = getPlaceholderDataUrl(asset.type, fileName);
            continue;
        }
        const fullPath = path.join(baseBuildDir, cleanUrl);
        try {
            // ✅ 简化逻辑：直接读取文件并判断是否需要压缩
            const buffer = await fs.readFile(fullPath);
            const originalSize = buffer.length;
            // 检查空文件，提供有效的占位符
            if (originalSize === 0) {
                console.warn(`⚠️  警告: 文件为空，使用占位符: ${asset.name || cleanUrl}`);
                file.url = getPlaceholderDataUrl(asset.type, fileName);
                file.size = 0;
                continue;
            }
            // 检查实际文件大小，跳过超大文件
            if (originalSize > SIZE_LIMIT) {
                skippedAssets.push(`${asset.name || assetId} (${(originalSize / 1024 / 1024).toFixed(2)}MB)`);
                asset.preload = false;
                // 提供有效的占位符数据，避免解析错误
                file.url = getPlaceholderDataUrl(asset.type, fileName);
                continue;
            }
            let dataUrl;
            let finalSize;
            // 处理 JSON 文件：去除注释和尾随逗号，验证格式
            if (asset.type === 'json' || fileName.endsWith('.json')) {
                try {
                    let jsonText = buffer.toString('utf-8');
                    // 移除单行注释 // ...
                    jsonText = jsonText.replace(/\/\/.*$/gm, '');
                    // 移除多行注释 /* ... */
                    jsonText = jsonText.replace(/\/\*[\s\S]*?\*\//g, '');
                    // 移除尾随逗号
                    jsonText = jsonText.replace(/,(\s*[}\]])/g, '$1');
                    // 验证是否为有效 JSON
                    const parsed = JSON.parse(jsonText);
                    const cleanJson = JSON.stringify(parsed);
                    dataUrl = `data:application/json;base64,${Buffer.from(cleanJson).toString('base64')}`;
                    finalSize = Buffer.from(cleanJson).length;
                }
                catch (error) {
                    console.warn(`⚠️  警告: JSON 文件格式无效，使用空对象: ${asset.name || cleanUrl}`);
                    const emptyJson = '{}';
                    dataUrl = `data:application/json;base64,${Buffer.from(emptyJson).toString('base64')}`;
                    finalSize = 2;
                }
            }
            // ✅ 核心优化：如果是图片，使用 sharp 压缩
            // ⚠️ 重要：字体纹理（type === 'font'）不能转换为 WebP！
            // MSDF/位图字体的 PNG 纹理包含精确的距离场/像素数据，
            // WebP 有损压缩会破坏这些数据导致字体渲染异常
            else if (isImageFile(cleanUrl)) {
                const isFontTexture = asset.type === 'font';
                const compressed = await compressImage(buffer, cleanUrl, {
                    convertToWebP: !isFontTexture, // 字体纹理不转换为 WebP
                    quality: isFontTexture ? 100 : 75, // 字体纹理使用无损压缩
                });
                // 防御性检查：如果压缩后的 buffer 为空，使用原始 buffer
                if (!compressed.buffer || compressed.buffer.length === 0) {
                    console.warn(`⚠️  警告: 压缩后的图片为空，使用原始文件: ${asset.name || cleanUrl}`);
                    const mime = guessMimeType(cleanUrl);
                    dataUrl = `data:${mime};base64,${buffer.toString('base64')}`;
                    finalSize = originalSize;
                }
                else {
                    const base64 = compressed.buffer.toString('base64');
                    // 额外检查：确保 base64 不为空
                    if (!base64 || base64.length === 0) {
                        console.warn(`⚠️  警告: base64 编码为空: ${asset.name || cleanUrl}`);
                        const mime = guessMimeType(cleanUrl);
                        dataUrl = `data:${mime};base64,${buffer.toString('base64')}`;
                        finalSize = originalSize;
                    }
                    else {
                        dataUrl = `data:${compressed.mime};base64,${base64}`;
                        finalSize = compressed.buffer.length;
                    }
                    // 记录优化效果
                    const savings = ((1 - finalSize / originalSize) * 100).toFixed(1);
                    optimizedAssets.push(`${asset.name || assetId}: ${(originalSize / 1024).toFixed(1)}KB → ${(finalSize / 1024).toFixed(1)}KB (-${savings}%)`);
                }
            }
            else {
                // 非图片文件，直接 base64 编码
                const mime = guessMimeType(cleanUrl);
                dataUrl = `data:${mime};base64,${buffer.toString('base64')}`;
                finalSize = originalSize;
            }
            // 更新 asset 配置
            file.url = dataUrl;
            file.size = finalSize;
            // data URL 不需要 hash/variants，避免引擎追加 ?t=hash 造成无效 URL
            if ('hash' in file) {
                delete file.hash;
            }
            if ('variants' in file) {
                delete file.variants;
            }
        }
        catch (error) {
            // 记录缺失资源详情，不只是简单警告
            missingAssets.push({
                name: asset.name || assetId,
                url: cleanUrl,
                type: asset.type || 'unknown',
                size: file.size,
            });
        }
    }
    // 输出统计信息
    if (optimizedAssets.length > 0) {
        console.log(`\n✅  通过 Vite 优化了 ${optimizedAssets.length} 个资源:`);
        optimizedAssets.forEach(info => console.log(`  - ${info}`));
    }
    if (skippedAssets.length > 0) {
        console.log(`\n⚠️  已跳过 ${skippedAssets.length} 个大型资源文件 (>1MB):`);
        skippedAssets.forEach(name => console.log(`  - ${name}`));
    }
    if (skippedScripts.length > 0) {
        console.log(`\n📦 跳过了 ${skippedScripts.length} 个脚本资源（已打包到 __game-scripts.js）`);
        if (skippedScripts.length <= 10) {
            skippedScripts.forEach(name => console.log(`  - ${name}`));
        }
        else {
            skippedScripts.slice(0, 5).forEach(name => console.log(`  - ${name}`));
            console.log(`  ... 还有 ${skippedScripts.length - 5} 个脚本`);
        }
    }
    // ⚠️ 重要：显示缺失资源的详细警告
    if (missingAssets.length > 0) {
        console.log('\n');
        console.warn(`⚠️  ==================== 缺失资源警告 ====================`);
        console.warn(`❌ 发现 ${missingAssets.length} 个资源文件不存在！`);
        console.warn(`   这些资源将不会被打包，可能导致游戏功能缺失！\n`);
        // 按类型分组显示
        const groupedByType = {};
        for (const asset of missingAssets) {
            const type = asset.type || 'unknown';
            if (!groupedByType[type]) {
                groupedByType[type] = [];
            }
            groupedByType[type].push(asset);
        }
        for (const [type, assets] of Object.entries(groupedByType)) {
            console.warn(`   [${type}] ${assets.length} 个:`);
            const displayCount = Math.min(assets.length, 5);
            for (let i = 0; i < displayCount; i++) {
                const asset = assets[i];
                const sizeInfo = asset.size ? ` (${(asset.size / 1024).toFixed(1)}KB)` : '';
                console.warn(`     - ${asset.name}${sizeInfo}`);
                console.warn(`       路径: ${asset.url}`);
            }
            if (assets.length > 5) {
                console.warn(`     ... 还有 ${assets.length - 5} 个 ${type} 资源`);
            }
        }
        console.warn(`\n   💡 建议:`);
        console.warn(`      1. 检查 Base Build 是否包含所有资源文件`);
        console.warn(`      2. 确认资源路径是否正确`);
        console.warn(`      3. 如果使用场景选择功能，确认所选场景包含所有依赖资源`);
        console.warn(`⚠️  ======================================================\n`);
    }
    return configJson;
}
/**
 * 为跳过的资源生成有效的占位符 data URL
 */
function getPlaceholderDataUrl(assetType, fileName) {
    // 1x1 透明 PNG 的 base64 编码（最小有效图片）
    const transparentPng = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
    // JSON 类型：返回空 JSON 对象
    if (assetType === 'json' || fileName.endsWith('.json')) {
        return `data:application/json;base64,${Buffer.from('{}').toString('base64')}`;
    }
    // 图片/纹理类型：返回 1x1 透明 PNG
    // 优先检查 assetType，因为文件名可能没有扩展名
    if (assetType === 'texture' || assetType === 'textureatlas' || isImageFile(fileName)) {
        return `data:image/png;base64,${transparentPng}`;
    }
    // 文本类型：返回单个空格的 base64
    const mime = guessMimeType(fileName);
    if (mime.startsWith('text/') || assetType === 'text' || assetType === 'html' || assetType === 'css') {
        return `data:${mime};base64,${Buffer.from(' ').toString('base64')}`;
    }
    // 默认：对于所有其他类型，使用 1x1 透明 PNG 作为通用占位符
    // 这对大多数二进制格式都是安全的
    return `data:image/png;base64,${transparentPng}`;
}
/**
 * 判断是否为图片文件
 */
function isImageFile(filePath) {
    const ext = path.extname(filePath).toLowerCase();
    return ['.png', '.jpg', '.jpeg', '.webp', '.gif'].includes(ext);
}
/**
 * 使用 sharp 压缩图片
 * @param buffer 原始图片 buffer
 * @param filePath 文件路径（用于判断格式）
 * @param options 压缩选项
 */
async function compressImage(buffer, filePath, options) {
    try {
        // 动态导入 sharp
        const sharp = (await import('sharp')).default;
        const ext = path.extname(filePath).toLowerCase();
        const convertToWebP = options?.convertToWebP !== false;
        let image = sharp(buffer);
        // 获取元数据
        const metadata = await image.metadata();
        if (convertToWebP && ['.png', '.jpg', '.jpeg'].includes(ext)) {
            // 转换为 WebP（节省 60-70% 空间）
            const quality = options?.quality ?? 75;
            const webpBuffer = await image
                .webp({ quality, effort: 6 })
                .toBuffer();
            // 防御性检查：如果压缩后为空，返回原始 buffer
            if (!webpBuffer || webpBuffer.length === 0) {
                console.warn(`警告: WebP 转换后为空，使用原始格式`);
                return { buffer, mime: guessMimeType(filePath) };
            }
            return {
                buffer: webpBuffer,
                mime: 'image/webp',
            };
        }
        // 按原格式压缩
        const quality = options?.quality ?? 80;
        switch (ext) {
            case '.png':
                // 如果 quality >= 100，使用无损压缩（适用于字体纹理）
                const pngBuffer = await image
                    .png({
                    compressionLevel: quality >= 100 ? 6 : 9, // 无损时使用中等压缩
                    palette: quality < 100, // 无损时不使用调色板
                })
                    .toBuffer();
                // 防御性检查
                if (!pngBuffer || pngBuffer.length === 0) {
                    console.warn(`警告: PNG 压缩后为空，使用原始文件`);
                    return { buffer, mime: 'image/png' };
                }
                return { buffer: pngBuffer, mime: 'image/png' };
            case '.jpg':
            case '.jpeg':
                const jpgBuffer = await image
                    .jpeg({ quality: options?.quality ?? 80, mozjpeg: true })
                    .toBuffer();
                // 防御性检查
                if (!jpgBuffer || jpgBuffer.length === 0) {
                    console.warn(`警告: JPEG 压缩后为空，使用原始文件`);
                    return { buffer, mime: 'image/jpeg' };
                }
                return { buffer: jpgBuffer, mime: 'image/jpeg' };
            default:
                // 其他格式不压缩
                return { buffer, mime: guessMimeType(filePath) };
        }
    }
    catch (error) {
        // 如果 sharp 失败，返回原始 buffer
        console.warn(`警告: sharp 压缩失败，使用原始文件:`, error);
        return { buffer, mime: guessMimeType(filePath) };
    }
}
/**
 * 从 Vite 返回的代码中提取 data URL
 */
function extractDataUrl(code) {
    // 格式 1: export default "data:..."
    let match = code.match(/export\s+default\s+["']([^"']*data:[^"']+)["']/);
    if (match)
        return match[1];
    // 格式 2: "data:..."
    match = code.match(/["']([^"']*data:[^"']+)["']/);
    if (match)
        return match[1];
    // 格式 3: 整个代码就是 data URL
    if (code.trim().startsWith('data:'))
        return code.trim();
    return null;
}
function guessMimeType(filePath) {
    const ext = path.extname(filePath).toLowerCase();
    switch (ext) {
        case '.png':
            return 'image/png';
        case '.jpg':
        case '.jpeg':
            return 'image/jpeg';
        case '.webp':
            return 'image/webp';
        case '.gif':
            return 'image/gif';
        case '.svg':
            return 'image/svg+xml';
        case '.glb':
            return 'model/gltf-binary';
        case '.gltf':
            return 'model/gltf+json';
        case '.json':
            return 'application/json';
        case '.js':
            return 'text/javascript';
        case '.css':
            return 'text/css';
        case '.wasm':
            return 'application/wasm';
        case '.ttf':
            return 'font/ttf';
        case '.otf':
            return 'font/otf';
        case '.mp3':
            return 'audio/mpeg';
        case '.wav':
            return 'audio/wav';
        case '.ogg':
            return 'audio/ogg';
        case '.mp4':
            return 'video/mp4';
        default:
            return 'application/octet-stream';
    }
}
function buildConfigValue(configJson) {
    const configText = JSON.stringify(configJson);
    return `"data:application/json;base64,${Buffer.from(configText).toString('base64')}"`;
}
function applyMraidConfig(configJson) {
    const next = { ...configJson };
    const props = { ...(next.application_properties || {}) };
    // Keep original fillMode (usually KEEP_ASPECT) for correct canvas scaling
    // The MRAID resize patch will handle the sizing
    // props.fillMode = 'NONE';  // Removed: causes canvas scaling issues
    next.application_properties = props;
    return next;
}
function stripAmmoAssets(configJson) {
    const next = { ...configJson };
    if (!next.assets) {
        return next;
    }
    const assets = { ...next.assets };
    for (const [id, asset] of Object.entries(assets)) {
        const assetData = asset;
        const moduleName = assetData?.data?.moduleName?.toLowerCase?.() || '';
        const fileUrl = assetData?.file?.url?.toLowerCase?.() || '';
        const fileName = assetData?.file?.filename?.toLowerCase?.() || '';
        const assetName = assetData?.name?.toLowerCase?.() || '';
        if (moduleName.includes('ammo') ||
            fileUrl.includes('ammo') ||
            fileName.includes('ammo') ||
            assetName.includes('ammo')) {
            console.log(`[ConfigGenerator] 移除 ammo 资源: ${id} - ${moduleName} ${fileName} ${assetName}`);
            delete assets[id];
        }
    }
    next.assets = assets;
    return next;
}
async function getEnginePatchScripts(options) {
    const scripts = [];
    if (options.patchXhrOut) {
        const patchCode = await readPatchFile('playcraft-no-xhr.js');
        const minified = await minifyPatchCode(patchCode, 'playcraft-no-xhr.js');
        scripts.push(`<script>${minified}</script>`);
    }
    else if (options.configJsonInline) {
        const patchCode = await readPatchFile('one-page-http-get.js');
        const minified = await minifyPatchCode(patchCode, 'one-page-http-get.js');
        scripts.push(`<script>${minified}</script>`);
    }
    if (options.inlineGameScripts) {
        const patchCode = await readPatchFile('one-page-inline-game-scripts.js');
        const minified = await minifyPatchCode(patchCode, 'one-page-inline-game-scripts.js');
        scripts.push(`<script>${minified}</script>`);
    }
    if (options.mraidSupport) {
        const patchCode = await readPatchFile('one-page-mraid-resize-canvas.js');
        const minified = await minifyPatchCode(patchCode, 'one-page-mraid-resize-canvas.js');
        scripts.push(`<script>${minified}</script>`);
    }
    // 注入 Scene applySettings 补丁：确保 settings.physics/render 存在，避免 "Cannot read properties of undefined (reading 'gravity')" 报错
    // 场景：ammoReplacement 或部分场景 JSON 缺少 physics 配置
    const scenePhysicsPatch = await readPatchFile('scene-physics-defaults.js');
    if (scenePhysicsPatch) {
        const minified = await minifyPatchCode(scenePhysicsPatch, 'scene-physics-defaults.js');
        scripts.push(`<script>${minified}</script>`);
    }
    // 如果启用了 config.json 压缩，需要注入 LZ4 解压运行时代码
    // 注意：必须在 compressEngine 之前检查，因为 compressEngine 也会注入 lz4.js
    if (options.compressConfigJson && !options.compressEngine) {
        // 如果没有启用引擎压缩，需要单独注入 lz4.js
        const lz4Script = await getLz4InlineScript();
        scripts.push(lz4Script);
    }
    // 注入 config.json 解压 patch（拦截 fetch 请求并解压 LZ4 数据）
    if (options.compressConfigJson) {
        const configDecompressPatch = buildConfigJsonDecompressPatch();
        scripts.push(`<script>${configDecompressPatch}</script>`);
    }
    if (options.compressEngine) {
        // Focus patch: 确保 canvas 获得焦点以接收键盘事件
        const focusPatch = `!function(){var e=function(){var e=document.getElementById("application-canvas");if(!e)return!1;try{e.focus()}catch(t){}e.addEventListener("pointerdown",function(){e.focus()}),e.addEventListener("click",function(){e.focus()});return!0},t=0;if(!e()){var n=setInterval(function(){(e()||++t>50)&&clearInterval(n)},100)}}();`;
        scripts.push(`<script>${focusPatch}</script>`);
        // Keyboard patch: 修复压缩后键盘事件不响应的问题
        // 1. 确保 pc.app.keyboard 存在
        // 2. 将键盘事件绑定到 window
        // 3. 关键修复：在轮询成功后也调用事件注册函数 r()
        const keyboardPatch = `!function(){var c=0,d=!1,i=function(){if(!window.pc||!window.pc.app||!window.pc.Keyboard)return!1;var a=window.pc.app;if(!a.keyboard)try{a.keyboard=new window.pc.Keyboard(window)}catch(x){}if(a.keyboard&&a.keyboard.attach)try{a.keyboard.attach(window)}catch(x){}var v=document.getElementById("application-canvas");if(v){v.tabIndex=0;try{v.focus()}catch(x){}}if(a.keyboard){r();return!0}return!1},r=function(){if(d||!window.pc||!window.pc.app||!window.pc.app.keyboard)return;var k=window.pc.app.keyboard;if(k._handleKeyDown){window.addEventListener("keydown",function(e){k._handleKeyDown(e)});window.addEventListener("keyup",function(e){k._handleKeyUp(e)});d=!0}};if(!i()){var o=setInterval(function(){if(i()||++c>50)clearInterval(o)},100)}}();`;
        scripts.push(`<script>${keyboardPatch}</script>`);
    }
    return scripts.join('\n');
}
async function getLz4InlineScript() {
    const lz4Code = await readPatchFile('lz4.js');
    return `<script>${lz4Code}</script>`;
}
async function buildCompressedEngineScript(engineCode) {
    const lz4 = await loadLz4Module();
    const compressed = lz4.compress(Buffer.from(engineCode));
    const base64 = Buffer.from(compressed).toString('base64');
    // 遵循 PlayCanvas 官方 one-page.js 的实现方式
    // 参考: https://github.com/playcanvas/playcanvas-rest-api-tools/blob/main/one-page.js
    // 官方使用 new Buffer() 因为 lz4.js 提供了 Buffer polyfill
    // 使用 textContent 而不是 innerText 以避免浏览器对内容的处理
    const wrapper = `!function(){var e=new Buffer("${base64}","base64"),n=Buffer.from(lz4.decompress(e)).toString(),r=document.createElement("script");r.async=!1,r.textContent=n,document.currentScript.parentNode.insertBefore(r,document.currentScript)}();`;
    return `<script>${wrapper}</script>`;
}
async function loadLz4Module() {
    const code = await readPatchFile('lz4.js');
    const require = createRequire(import.meta.url);
    const sandboxWindow = {};
    const factory = new Function('require', 'window', 'globalThis', `${code}; return window.lz4 || globalThis.lz4;`);
    return factory(require, sandboxWindow, sandboxWindow);
}
async function readPatchFile(name) {
    const currentDir = path.dirname(fileURLToPath(import.meta.url));
    const patchPath = path.resolve(currentDir, '../../templates/patches', name);
    return await fs.readFile(patchPath, 'utf-8');
}
/**
 * 格式化字节数为人类可读格式
 */
function formatBytes(bytes) {
    if (bytes === 0)
        return '0 B';
    const k = 1024;
    const sizes = ['B', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
}
/**
 * 构建 config.json 解压 patch
 *
 * 这个 patch 拦截 PlayCanvas 的 pc.Http.prototype.get 请求，
 * 检测是否为 LZ4 压缩的 config.json，如果是则解压后返回。
 *
 * 工作原理：
 * 1. 保存原始 pc.Http.prototype.get 函数
 * 2. 重写 get，检测 URL 是否为 LZ4 压缩的 JSON data URL
 * 3. 如果是，解压并通过 callback 返回解析后的 JSON 对象
 *
 * 注意：
 * - PlayCanvas 使用 pc.Http.prototype.get 加载 config.json，不是 fetch
 * - 使用 lz4.js 提供的 Buffer polyfill 进行解压
 * - 必须在 pc 对象可用后才能生效，所以使用轮询等待
 */
function buildConfigJsonDecompressPatch() {
    // 原始代码:
    // (function() {
    //   function patch() {
    //     if (!window.pc || !window.pc.Http) return false;
    //     var oldGet = pc.Http.prototype.get;
    //     pc.Http.prototype.get = function(url, options, callback) {
    //       if (typeof options === 'function') {
    //         callback = options;
    //         options = {};
    //       }
    //       if (typeof url === 'string' && url.startsWith('data:application/x-lz4-json;base64,')) {
    //         try {
    //           var base64 = url.replace('data:application/x-lz4-json;base64,', '');
    //           var compressed = new Buffer(base64, 'base64');
    //           var decompressed = lz4.decompress(compressed);
    //           var str = Buffer.from(decompressed).toString('utf8');
    //           var json = JSON.parse(str);
    //           console.log('[LZ4 Config] Decompressed config.json successfully, size:', str.length);
    //           callback(null, json);
    //         } catch(e) {
    //           console.error('[LZ4 Config] Decompression error:', e);
    //           callback(e);
    //         }
    //         return;
    //       }
    //       oldGet.call(this, url, options, callback);
    //     };
    //     return true;
    //   }
    //   if (!patch()) {
    //     var c = 0;
    //     var i = setInterval(function() {
    //       if (patch() || ++c > 100) clearInterval(i);
    //     }, 10);
    //   }
    // })();
    return `!function(){function p(){if(!window.pc||!pc.Http)return!1;var o=pc.Http.prototype.get;return pc.Http.prototype.get=function(e,t,n){"function"==typeof t&&(n=t,t={});if("string"==typeof e&&e.startsWith("data:application/x-lz4-json;base64,")){try{var r=e.replace("data:application/x-lz4-json;base64,",""),a=new Buffer(r,"base64"),d=lz4.decompress(a),s=Buffer.from(d).toString("utf8"),j=JSON.parse(s);console.log("[LZ4 Config] Decompressed config.json successfully, size:",s.length);n(null,j)}catch(x){console.error("[LZ4 Config] Decompression error:",x);n(x)}return}o.call(this,e,t,n)},!0}if(!p()){var c=0,i=setInterval(function(){(p()||++c>100)&&clearInterval(i)},10)}}();`;
}
