// Fetch & self-host the web fonts used by the 5 font styles (styles.css [data-font]).
// Downloads chunked woff2 from jsDelivr into app/fonts/<dir>/files/, and generates
// app/css/fonts.css with @font-face rules pointing at the local /fonts/ route.
//
// Run from anywhere:  node tools/fetch-fonts.mjs
// Requires Node 18+ (global fetch). Re-run to refresh fonts.
import { mkdir, writeFile, rm } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import path from 'node:path';

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const FONTS_DIR = path.join(ROOT, 'app', 'fonts');
const OUT_CSS = path.join(ROOT, 'app', 'css', 'fonts.css');
const CDN = 'https://cdn.jsdelivr.net/npm';

// Per-font manifest. `dir` doubles as the woff2 filename slug. Latin UI fonts use the
// variable packages (single file, all weights); CJK fonts use static 400/700 chunks.
const FONTS = [
  { dir: 'inter',            pkg: '@fontsource-variable/inter',            family: 'Inter',            css: ['index.css'], variable: true, subsets: ['latin', 'latin-ext'] },
  { dir: 'nunito',           pkg: '@fontsource-variable/nunito',           family: 'Nunito',           css: ['index.css'], variable: true, subsets: ['latin', 'latin-ext'] },
  { dir: 'jetbrains-mono',   pkg: '@fontsource-variable/jetbrains-mono',   family: 'JetBrains Mono',   css: ['index.css'], variable: true, subsets: ['latin', 'latin-ext'] },
  { dir: 'noto-sans-jp',     pkg: '@fontsource/noto-sans-jp',     family: 'Noto Sans JP',     css: ['index.css', 'japanese.css'], weights: ['400', '700'], subsets: ['latin', 'latin-ext', 'japanese'] },
  { dir: 'm-plus-rounded-1c', pkg: '@fontsource/m-plus-rounded-1c', family: 'M PLUS Rounded 1c', css: ['index.css', 'japanese.css'], weights: ['400', '700'], subsets: ['latin', 'latin-ext', 'japanese'] },
  // 文楷 (cn-font-split micro-chunked, ~190 blocks). Keep every block; family already matches the stack.
  { dir: 'wenkai',           pkg: 'lxgw-wenkai-screen-webfont@1.7.0',      family: 'LXGW WenKai Screen', css: ['lxgwwenkaiscreen.css'], keepAll: true },
];

async function fetchText(url) {
  const r = await fetch(url);
  if (!r.ok) throw new Error(`GET ${url} -> ${r.status}`);
  return r.text();
}
async function fetchBuf(url) {
  const r = await fetch(url);
  if (!r.ok) throw new Error(`GET ${url} -> ${r.status}`);
  return Buffer.from(await r.arrayBuffer());
}

// Run async tasks with bounded concurrency.
async function pool(items, limit, fn) {
  const q = [...items];
  const workers = Array.from({ length: Math.min(limit, q.length) }, async () => {
    while (q.length) await fn(q.shift());
  });
  await Promise.all(workers);
}

// Split a fontsource/cn-font-split stylesheet into individual @font-face blocks.
function splitFaces(css) {
  return [...css.matchAll(/@font-face\s*\{[^}]*\}/g)].map(m => m[0]);
}
function field(block, name) {
  const m = block.match(new RegExp(name + '\\s*:\\s*([^;}]+)', 'i'));
  return m ? m[1].trim() : '';
}
function woff2Name(block) {
  const m = block.match(/url\(['"]?([^'")]+?\.woff2)['"]?\)/i);
  return m ? path.basename(m[1]) : '';
}
// Parse `<slug>-<subset>-<weight>-<style>.woff2` -> {subset, weight}.
function parseName(file, slug) {
  let base = file.replace(/\.woff2$/, '').replace(/-(normal|italic)$/, '');
  const wm = base.match(/-(\d{2,3}|wght)$/);
  const weight = wm ? wm[1] : '';
  base = base.replace(/-(\d{2,3}|wght)$/, '');
  const subset = base.startsWith(slug + '-') ? base.slice(slug.length + 1) : base;
  return { subset, weight };
}

async function run() {
  await rm(FONTS_DIR, { recursive: true, force: true });
  const cssParts = ['/* Auto-generated by tools/fetch-fonts.mjs — do not edit by hand. */\n'];
  const downloads = []; // {url, dest}

  for (const f of FONTS) {
    const kept = [];
    for (const cssFile of f.css) {
      const sheet = await fetchText(`${CDN}/${f.pkg}/${cssFile}`);
      for (const block of splitFaces(sheet)) {
        const file = woff2Name(block);
        if (!file) continue;
        if (!f.keepAll) {
          const { subset, weight } = parseName(file, f.dir);
          if (f.subsets && !f.subsets.includes(subset)) continue;
          if (f.weights && weight && !f.weights.includes(weight)) continue;
        }
        const range = field(block, 'unicode-range');
        const weight = field(block, 'font-weight') || '400';
        const style = field(block, 'font-style') || 'normal';
        const fmt = /woff2-variations/.test(block) ? 'woff2-variations' : 'woff2';
        kept.push(`@font-face{font-family:'${f.family}';font-style:${style};font-weight:${weight};font-display:swap;src:url(/fonts/${f.dir}/files/${file}) format('${fmt}');${range ? `unicode-range:${range};` : ''}}`);
        downloads.push({ url: `${CDN}/${f.pkg}/files/${file}`, dest: path.join(FONTS_DIR, f.dir, 'files', file) });
      }
    }
    // De-dup woff2 (a file may appear in multiple css); keep unique faces too.
    cssParts.push(`/* ${f.family} (${f.dir}) — ${kept.length} faces */`, ...[...new Set(kept)], '');
    console.log(`${f.family}: ${kept.length} faces`);
  }

  // Download unique files with bounded concurrency.
  const uniq = [...new Map(downloads.map(d => [d.dest, d])).values()];
  await mkdir(path.dirname(OUT_CSS), { recursive: true });
  let done = 0;
  await pool(uniq, 16, async (d) => {
    await mkdir(path.dirname(d.dest), { recursive: true });
    await writeFile(d.dest, await fetchBuf(d.url));
    if (++done % 25 === 0 || done === uniq.length) console.log(`  downloaded ${done}/${uniq.length}`);
  });

  await writeFile(OUT_CSS, cssParts.join('\n'), 'utf8');
  console.log(`\nWrote ${OUT_CSS} and ${uniq.length} woff2 files under ${FONTS_DIR}`);
}

run().catch(e => { console.error(e); process.exit(1); });
