#!/usr/bin/env node
/**
 * 项目运行包：扁平结构，直接供 scratch-gui static/unity 本地加载。
 *
 *   node tools/package-for-project.js <cocosBuildDir> <outDir> [options]
 *
 * 选项:
 *   --minify-db          压缩 levels-database.json（默认开启）
 *   --no-minify-db       保留格式化 JSON
 *   --brotli-db          额外生成 levels-database.json.br
 *   --preload-resources  启动时预加载 resources 包（默认关闭，按需加载）
 *   --preload-levels     启动时预加载 level-prefabs 包（默认关闭）
 *   --report             打印体积报告（默认开启）
 *   --no-report
 *
 * 输出结构:
 *   Build/mstest5.loader.js + 占位 .br
 *   index.js, application.js, cocos-bridge.js
 *   cocos-js/, src/, assets/
 */
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const {
  minifyLevelsDatabase,
  brotliCompressFile,
  patchPreloadSettings,
  patchSplashSettings,
  printPackageReport,
  formatBytes,
} = require('./package-optimize');

const argv = process.argv.slice(2);
const positional = [];
const flags = new Set();
for (const a of argv) {
  if (a.startsWith('--')) flags.add(a);
  else positional.push(a);
}

const buildDir = path.resolve(positional[0] || '');
const outDir = path.resolve(positional[1] || '');
const opts = {
  minifyDb: !flags.has('--no-minify-db'),
  brotliDb: flags.has('--brotli-db'),
  preloadResources: flags.has('--preload-resources'),
  preloadLevelPrefabs: flags.has('--preload-levels'),
  report: !flags.has('--no-report'),
};
const PRODUCT = 'mstest5';
const UNITY_BASE = '/unity/';
const templateDir = path.join(__dirname, '../web-template');

if (!buildDir || !outDir) {
  console.error('Usage: package-for-project.js <cocosBuildDir> <outDir> [--minify-db] [--brotli-db] ...');
  process.exit(1);
}

function rmrf(p) {
  if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
}
function mkdirp(p) { fs.mkdirSync(p, { recursive: true }); }
function copyFile(s, d) { mkdirp(path.dirname(d)); fs.copyFileSync(s, d); }
function copyDir(s, d) {
  if (!fs.existsSync(s)) return;
  mkdirp(d);
  for (const ent of fs.readdirSync(s, { withFileTypes: true })) {
    const sp = path.join(s, ent.name);
    const dp = path.join(d, ent.name);
    if (ent.isDirectory()) copyDir(sp, dp);
    else copyFile(sp, dp);
  }
}
function patchText(file, fn) {
  const t = fs.readFileSync(file, 'utf8');
  fs.writeFileSync(file, fn(t), 'utf8');
}
function brotliStub(dst) {
  fs.writeFileSync(dst, zlib.brotliCompressSync(Buffer.from([0])));
}

const required = ['index.js', 'application.js', 'src', 'cocos-js', 'assets'];
for (const r of required) {
  if (!fs.existsSync(path.join(buildDir, r))) {
    console.error('Missing in Cocos build:', r, 'at', buildDir);
    process.exit(1);
  }
}

console.log('>>> Cocos 构建:', buildDir);
console.log('>>> 输出（项目运行包）:', outDir);
console.log('>>> 优化:', JSON.stringify({
  minifyDb: opts.minifyDb,
  brotliDb: opts.brotliDb,
  preloadResources: opts.preloadResources,
  preloadLevelPrefabs: opts.preloadLevelPrefabs,
}));

mkdirp(outDir);
for (const item of [
  'Build', 'index.js', 'application.js', 'cocos-bridge.js',
  'cocos-js', 'src', 'assets',
  'StreamingAssets', 'TemplateData', 'index.html',
  'levels-database.json', 'levels-database.json.br',
]) {
  rmrf(path.join(outDir, item));
}

for (const item of ['index.js', 'application.js']) {
  copyFile(path.join(buildDir, item), path.join(outDir, item));
}
copyDir(path.join(buildDir, 'src'), path.join(outDir, 'src'));
copyDir(path.join(buildDir, 'cocos-js'), path.join(outDir, 'cocos-js'));
copyDir(path.join(buildDir, 'assets'), path.join(outDir, 'assets'));

const EMBEDDED_RESOLUTION_PATCH =
  '!(function(){v.resizeWithBrowserSize(!0);v.setDesignResolutionSize(G,H,C.FIXED_WIDTH)})()';

function patchMainIndexRuntime(t) {
  return t
    .replace(/return\[\]\.concat\(l\)\},entityFlipX/g, 'return Array.from(l)},entityFlipX')
    .replace(/return\[\]\.concat\(n\)\},e\.ensureUILayerTree/g, 'return Array.from(n)},e.ensureUILayerTree')
    .replace(/e\("applyEmbeddedDesignResolution",\(function\(\)\{[^}]+\}\)\)/g, 'e("applyEmbeddedDesignResolution",(function(){t.resizeWithBrowserSize(!0),t.setDesignResolutionSize(s,o,n.FIXED_WIDTH)}))')
    .replace(/v\.setDesignResolutionSize\(G,H,C\.(?:SHOW_ALL|FIXED_WIDTH|FIXED_HEIGHT)\)/g, EMBEDDED_RESOLUTION_PATCH)
    .replace(/setDesignResolutionSize\(G,H,C\.(?:SHOW_ALL|FIXED_WIDTH|FIXED_HEIGHT)\)/g, EMBEDDED_RESOLUTION_PATCH)
    .replace(
      /e\.syncMapDataComponent=function\(e,n\)\{var r,t,o=e\.getComponent\(p\);o&&\(o\.levelID=n\.levelID,o\.theme=T\(n\),o\.groundJson=JSON\.stringify\(null!=\(r=n\.ground\)\?r:\{\}\),o\.borderJson=JSON\.stringify\(null!=\(t=n\.border\)\?t:\{\}\)\)\}/,
      'e.syncMapDataComponent=function(e,n){var r,t,o=e.getComponent(p);if(!o)return;o.levelID=n.levelID;var i=n.theme&&String(n.theme).trim();o.theme=i?T(n):o.theme||T(n);r=n.ground;r&&Object.keys(r).length&&(o.groundJson=JSON.stringify(r));t=n.border;t&&Object.keys(t).length&&(o.borderJson=JSON.stringify(t))}',
    )
    .replace(
      /e\.prepare=function\(\)\{var e=r\(\(function\*\(e,n\)\{e\.name="Level_"\+n\.levelID,f\.purgeRuntimeGrids\(e\),this\.ensureUILayerTree\(e\);var r=T\(n\),t=m\(e,n\);/,
      'e.prepare=function(){var e=r((function*(e,n){e.name="Level_"+n.levelID,f.purgeRuntimeGrids(e),this.ensureUILayerTree(e);var _md=e.getComponent(p);if(_md){try{var _g=JSON.parse(_md.groundJson||"{}");(!n.ground||!Object.keys(n.ground).length)&&Object.keys(_g).length&&(n.ground=_g);var _b=JSON.parse(_md.borderJson||"{}");(!n.border||!Object.keys(n.border).length)&&Object.keys(_b).length&&(n.border=_b)}catch(_e){}(!n.theme||!String(n.theme).trim())&&_md.theme&&(n.theme=_md.theme)}var r=T(n),t=m(e,n);',
    )
    .replace(
      /function p\(e\)\{var r=e\.children\.filter\(\(function\(e\)\{return e\.isValid&&y\.test\(e\.name\)\}\)\);/,
      'function p(e){if(!e||!e.isValid||!e.children)return;var r=e.children.filter((function(e){return e.isValid&&y.test(e.name)}));',
    )
    .replace(
      /this\.applyMapDataFromConfig\(n\),yield J\.prepare\(o,n\);var s=n\.theme\|\|"silu";/,
      'yield J.prepare(o,n),this.applyMapDataFromConfig(n),this.curConfig=n;var s=n.theme||"silu";',
    )
    .replace(
      /c\.moveTowards\(i,e,this\.targetPosition,this\.moveSpeed\*t\)/g,
      'c.moveTowards(i,e,this.targetPosition,this.moveSpeed*t*((null!=(S=globalThis.__tfrhGameSpeed)&&S>0)?S:1))',
    )
    .replace(
      /if\(!\(t<=0\)\)if\(this\.frameTimer\+=e,!\(this\.frameTimer<t\)\)/,
      'if(!(t<=0))if(this.frameTimer+=e*((null!=(z=globalThis.__tfrhGameSpeed)&&z>0)?z:1),!(this.frameTimer<t))',
    )
    .replace(
      /a\.onPlaySpeedChange=function\(\)\{this\.speedIndex=\(this\.speedIndex\+1\)%l\.SPEEDS\.length,d\.globalGameTimeScale=l\.SPEEDS\[this\.speedIndex\],this\.setPlaySpeedSprite\(this\.resolveHudTheme\(\)\)\}/,
      'a.onPlaySpeedChange=function(){this.speedIndex=(this.speedIndex+1)%l.SPEEDS.length;var e=l.SPEEDS[this.speedIndex];d.globalGameTimeScale=e,globalThis.__tfrhGameSpeed=e,console.log("[UIMain] 倍速 x"+e),this.setPlaySpeedSprite(this.resolveHudTheme())}',
    )
    .replace(
      /a\.onLoad=function\(\)\{d\.globalGameTimeScale=l\.SPEEDS\[this\.speedIndex\],this\.buildUI\(\)/,
      'a.onLoad=function(){d.globalGameTimeScale=l.SPEEDS[this.speedIndex],globalThis.__tfrhGameSpeed=l.SPEEDS[this.speedIndex],this.buildUI()',
    )
    .replace(
      /a\.onRevertGame=function\(\)\{var e;this\.speedIndex=0,d\.globalGameTimeScale=l\.SPEEDS\[this\.speedIndex\],null==\(e=w\.instance\)\|\|e\.resetLevel\(\)\}/,
      'a.onRevertGame=function(){var e;this.speedIndex=0,d.globalGameTimeScale=l.SPEEDS[this.speedIndex],globalThis.__tfrhGameSpeed=l.SPEEDS[this.speedIndex],null==(e=w.instance)||e.resetLevel()}',
    );
}

const mainIndex = path.join(outDir, 'assets', 'main', 'index.js');
if (fs.existsSync(mainIndex)) {
  patchText(mainIndex, (t) => patchMainIndexRuntime(t));
}

copyFile(
  path.join(templateDir, 'cocos-bridge.js'),
  path.join(outDir, 'cocos-bridge.js'),
);

const levelsDbSrc = path.join(__dirname, '../assets/level-data/levels-database.json');
const levelsDbDst = path.join(outDir, 'levels-database.json');
if (fs.existsSync(levelsDbSrc)) {
  if (opts.minifyDb) {
    const { before, after } = minifyLevelsDatabase(levelsDbSrc, levelsDbDst);
    console.log(`>>> levels-database.json 压缩: ${formatBytes(before)} → ${formatBytes(after)}`);
  } else {
    copyFile(levelsDbSrc, levelsDbDst);
  }
  if (opts.brotliDb) {
    const { raw, br } = brotliCompressFile(levelsDbDst, path.join(outDir, 'levels-database.json.br'));
    console.log(`>>> levels-database.json.br: ${formatBytes(raw)} → ${formatBytes(br)}`);
  }
  console.log('>>> 已附带 levels-database.json（Cocos 关卡库）');
} else {
  console.warn('>>> 警告: 未找到 levels-database.json，请先运行 tools/sync-level-db.sh');
}

patchText(path.join(outDir, 'application.js'), (t) =>
  t.replace(/this\.settingsPath\s*=\s*'[^']*'/, `this.settingsPath = '${UNITY_BASE}src/settings.json'`),
);

function patchSettingsForFrontend(settingsFile) {
  patchText(settingsFile, (t) => {
    const j = JSON.parse(t);
    j.assets = j.assets || {};
    j.assets.server = UNITY_BASE;
    patchPreloadSettings(j, {
      preloadResources: opts.preloadResources,
      preloadLevelPrefabs: opts.preloadLevelPrefabs,
    });
    patchSplashSettings(j, { stripSplashAssets: true });
    if (j.rendering) {
      j.rendering.effectSettingsPath = `${UNITY_BASE}src/effect.bin`;
    }
    if (j.scripting && Array.isArray(j.scripting.scriptPackages)) {
      j.scripting.scriptPackages = j.scripting.scriptPackages.map((p) => {
        const rel = String(p).replace(/^\.\.\//, '').replace(/^\//, '');
        return `${UNITY_BASE}${rel}`;
      });
    }
    return JSON.stringify(j);
  });
}

const settingsPath = path.join(outDir, 'src', 'settings.json');
if (fs.existsSync(settingsPath)) {
  patchSettingsForFrontend(settingsPath);
  const preload = JSON.parse(fs.readFileSync(settingsPath, 'utf8')).assets?.preloadBundles || [];
  console.log('>>> 预加载 bundles:', preload.map((b) => b.bundle).join(', ') || '(none)');
}

const importMapPath = path.join(outDir, 'src', 'import-map.json');
if (fs.existsSync(importMapPath)) {
  patchText(importMapPath, (t) => {
    const j = JSON.parse(t);
    j.imports = j.imports || {};
    if (j.imports.cc) j.imports.cc = `${UNITY_BASE}cocos-js/cc.js`;
    return JSON.stringify(j);
  });
}

const buildOut = path.join(outDir, 'Build');
mkdirp(buildOut);
const loaderTpl = fs.readFileSync(path.join(templateDir, 'mstest5-cocos-loader.js'), 'utf8');
fs.writeFileSync(
  path.join(buildOut, `${PRODUCT}.loader.js`),
  loaderTpl.replace(/__PRODUCT_NAME__/g, PRODUCT),
  'utf8',
);
for (const stub of ['data', 'framework.js', 'wasm']) {
  brotliStub(path.join(buildOut, `${PRODUCT}.${stub}.br`));
}

if (opts.report) {
  const preloadNote = [
    'main',
    opts.preloadResources ? 'resources' : null,
    opts.preloadLevelPrefabs ? 'level-prefabs' : null,
  ].filter(Boolean).join(', ');
  printPackageReport(outDir, { preloadNote });
  const hasLevelBundle = fs.existsSync(path.join(outDir, 'assets', 'level-prefabs'));
  if (!hasLevelBundle) {
    console.log('\n>>> 提示: 尚未拆分 level-prefabs 分包。运行 migrate-level-prefab-bundle.sh 后重建可显著减小 resources 体积。');
  }
}

console.log('\n>>> 项目运行包文件:');
for (const rel of [
  'Build/mstest5.loader.js',
  'index.js',
  'application.js',
  'cocos-bridge.js',
  'cocos-js/',
  'src/',
  'assets/',
  'levels-database.json',
]) {
  console.log('   ', rel);
}
console.log('\n完成。flat 包与 CDN 不一致，生产请用: bash tools/package-for-project.sh');
console.log('  ~/tfrh/001code/001code-html/scratch-gui/static/unity/import-to-unity.sh', outDir);
