#!/usr/bin/env node
/**
 * tools/preprocess.mjs — Offline GLB optimization for hmwebviewer.
 *
 * SCOPE: GLB/glTF ONLY (gltf-transform pipeline). Other formats shipped as
 * samples (OBJ/DAE/FBX/IFC) are NOT processed here — they are consumed raw by
 * their respective three.js loaders; no offline Draco/KTX2 step applies.
 *
 * Compresses a glTF/GLB with Draco (geometry) + WebP texture compression in one
 * pass using gltf-transform. Output is a single self-contained .optimized.glb
 * ready for the viewer's GLTFLoader (+ DRACOLoader).
 *
 * KTX2/Basis texture encoding is NOT done here — it needs the `toktx` platform
 * encoder (KTX-Software 4.3+). gltf-transform exposes it via the CLI `etc1s`
 * (lossy, smaller) or `uastc` (higher quality) commands, e.g.:
 *     gltf-transform etc1s <out.optimized.glb> <out.ktx2.glb>
 * Install KTX-Software from https://github.com/KhronosGroup/KTX-Software first.
 * Runtime: the viewer's KTX2Loader decodes these at load (verified with the
 * Khronos ABeautifulGame KTX2+Draco sample — 626ms perceived load).
 *
 * USAGE
 *   node tools/preprocess.mjs <input.glb> [output.glb] [--draco-bits N] [--texture FMT]
 *
 *   --draco-bits N   Position quantization bits (default 14). 8..16; lower=smaller/lossier.
 *   --texture FMT    Texture re-encode: webp (default) | jpeg | png | none.
 *
 * OUTPUT
 *   Default output = input with `.optimized.glb` suffix. Prints before/after
 *   byte sizes + reduction %.
 *
 * INSTALL (these are devDependencies now; if absent the script prints the
 * install command and exits non-zero — no silent skip):
 *   npm install -D @gltf-transform/core @gltf-transform/functions \
 *                   @gltf-transform/extensions @gltf-transform/cli
 */

async function loadDeps() {
  let core, fns, ext;
  try {
    core = await import('@gltf-transform/core');
    fns = await import('@gltf-transform/functions');
    ext = await import('@gltf-transform/extensions');
  } catch (e) {
    const msg = (e && e.message) ? e.message : String(e);
    console.error('[preprocess] Missing required package: ' + msg);
    console.error('[preprocess] Install the toolchain with:');
    console.error(
      '  npm install -D @gltf-transform/core @gltf-transform/functions ' +
        '@gltf-transform/extensions @gltf-transform/cli'
    );
    process.exit(1);
  }
  return { core, fns, ext };
}

const TEXTURE_FORMATS = ['webp', 'jpeg', 'png', 'none'];

function parseArgs(argv) {
  const args = argv.slice(2);
  if (args.length === 0 || args[0] === '-h' || args[0] === '--help') {
    return { help: true };
  }
  const positional = [];
  let dracoBits = 14;
  let texture = 'webp';
  for (let i = 0; i < args.length; i++) {
    const a = args[i];
    if (a === '--draco-bits') {
      const v = Number(args[++i]);
      if (!Number.isInteger(v) || v < 8 || v > 16) {
        throw new Error('--draco-bits must be an integer 8..16');
      }
      dracoBits = v;
    } else if (a === '--texture') {
      const v = String(args[++i]).toLowerCase();
      if (!TEXTURE_FORMATS.includes(v)) {
        throw new Error('--texture must be one of: ' + TEXTURE_FORMATS.join(', '));
      }
      texture = v;
    } else if (a.startsWith('--')) {
      throw new Error('Unknown option: ' + a);
    } else {
      positional.push(a);
    }
  }
  if (positional.length === 0) {
    throw new Error('Missing <input.glb>');
  }
  const input = positional[0];
  let output = positional[1];
  if (!output) {
    output = input.replace(/\.glb$/i, '') + '.optimized.glb';
  }
  return { input, output, dracoBits, texture };
}

function usage() {
  console.error(
    'Usage: node tools/preprocess.mjs <input.glb> [output.glb] [--draco-bits N] [--texture webp|jpeg|png|none]'
  );
}

async function main() {
  const opts = parseArgs(process.argv);
  if (opts.help) {
    usage();
    return;
  }

  const { core, fns, ext } = await loadDeps();
  const { WebIO } = core;
  const { quantize, dedup, weld, draco, textureCompress, prune } = fns;
  const { KHRDracoMeshCompression, EXTTextureWebP } = ext;

  const { promises: fs } = await import('node:fs');
  const path = await import('node:path');

  const inputPath = path.resolve(opts.input);
  const outputPath = path.resolve(opts.output);

  let inputBytes;
  try {
    inputBytes = await fs.readFile(inputPath);
  } catch (e) {
    console.error('[preprocess] Cannot read input "' + inputPath + '": ' + e.message);
    process.exit(1);
  }
  const beforeSize = inputBytes.byteLength;

  // Draco encoder must be handed to the extension as a registered dependency.
  const draco3d = await import('draco3d');
  const dracoEncoder = await draco3d.createEncoderModule();
  const io = new WebIO()
    .registerExtensions([KHRDracoMeshCompression, EXTTextureWebP])
    .registerDependencies({ 'draco3d.encoder': dracoEncoder });

  let doc;
  try {
    doc = await io.readBinary(inputBytes);
  } catch (e) {
    console.error('[preprocess] Failed to parse glTF: ' + e.message);
    process.exit(1);
  }

  const transforms = [];
  transforms.push(dedup());
  transforms.push(weld({ tolerance: 1e-4 }));
  transforms.push(
    quantize({
      quantizePosition: opts.dracoBits,
      quantizeNormal: 10,
      quantizeTexcoord: 12,
      quantizeColor: 8,
    })
  );
  transforms.push(prune());
  transforms.push(
    draco({
      encodeSpeed: 5,
      decodeSpeed: 5,
      quantizePosition: opts.dracoBits,
      quantizeNormal: 10,
      quantizeTexcoord: 12,
      quantizeColor: 8,
    })
  );
  if (opts.texture !== 'none') {
    transforms.push(textureCompress({ targetFormat: opts.texture, quality: 8 }));
  }

  await doc.transform(...transforms);

  const outGLB = await io.writeBinary(doc);
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
  await fs.writeFile(outputPath, outGLB);
  const afterSize = outGLB.byteLength;

  const reduction = beforeSize > 0 ? ((1 - afterSize / beforeSize) * 100) : 0;
  const fmt = (n) => (n / 1024).toFixed(1) + ' KiB';
  console.log('[preprocess] ' + path.basename(inputPath) + ' -> ' + path.basename(outputPath));
  console.log('[preprocess]   before: ' + beforeSize + ' bytes (' + fmt(beforeSize) + ')');
  console.log('[preprocess]   after:  ' + afterSize + ' bytes (' + fmt(afterSize) + ')');
  console.log(
    '[preprocess]   reduction: ' + reduction.toFixed(1) + '%  ' +
      '(draco-bits=' + opts.dracoBits + ', texture=' + opts.texture + ')'
  );
}

main().catch((e) => {
  console.error('[preprocess] Fatal: ' + (e && e.stack ? e.stack : e));
  process.exit(1);
});
