/**
 * Regenerate the share cards.
 *
 *   node scripts/brand-assets/capture.mjs
 *
 * Two passes, because the cards are backed by the running app rather than by a
 * drawing of it. First it shoots the city and the office out of a built `dist/`;
 * then it renders `og.html` over those shots at exactly 1200x630 and writes the
 * two PNGs into `public/`, from where Vite copies them verbatim.
 *
 * Rasterising with headless Chromium rather than a converter is the convention
 * `lumbridge-v4/scripts/brand-assets/README.md` already set on this box, for the
 * reason it gives: there is no ImageMagick, no `rsvg-convert` and no `sharp`
 * here, and a browser renders the CSS the card was designed in anyway.
 *
 * ### Why the art is a screenshot and not an illustration
 *
 * Because the thing is worth looking at, and because an illustration of it goes
 * stale silently. The card that shipped on lumbridgecorp.com was a viewport
 * screenshot of a marketing page that had since been rewritten, so the preview
 * advertised a positioning the site no longer used and nothing noticed for a
 * month. A card regenerated from `dist/` by one command is a card that can be
 * kept true by running that command.
 */

import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { serve, launch, clockShim, FURNITURE, hide } from "./harness.mjs";

const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, "..", "..");
const PUBLIC = join(ROOT, "public");

/**
 * Midday, fixed.
 *
 * The sun is real — `observe()` computes it from `new Date()` — so a card
 * regenerated at two in the morning is an honest photograph of a black
 * rectangle. The clock is shifted rather than frozen because the app drives
 * everything else off `requestAnimationFrame`, and a stopped clock stalls the
 * frame loop the screenshot is waiting on.
 */
const NOON = "2026-08-06T12:40:00-07:00";

async function shootApp(browser, url, file, { key = null, settle = 20000 } = {}) {
  // 2x, so the art is still sharp when a timeline shows the card at 600px wide
  // on a retina screen.
  const page = await browser.newPage({
    viewport: { width: 1400, height: 900 },
    deviceScaleFactor: 2,
    /**
     * Without this the card was framed differently every run, and had been
     * since the day it was written.
     *
     * `scenekit.ts` eases a chapter change over about two seconds of scene
     * time and `stage.ts` clamps `dt` to 50 ms a frame — so a flight is about
     * forty frames however fast they are drawn, and the four seconds this
     * waited caught the camera partway across the bay at a different point
     * each time. Two consecutive captures of an unchanged repo produced two
     * different cards, neither of them the chapter the key press had asked
     * for. It was worst under software GL, where forty frames took twenty
     * seconds; on the GPU it is a shorter race and still a race.
     *
     * Reduced motion is the app's own answer for "somebody clicked a name in a
     * list": `flyTo` sets the pose outright, so the shutter opens on the pose
     * the key names.
     *
     * The *framing* is what this fixes, not the bytes. Aircraft are still
     * crossing and cloud shadow is still drifting, so two runs differ by a few
     * pixels and `git diff` will always show the PNG as changed. That is the
     * app being alive, and worth far less than the framing being on purpose.
     */
    reducedMotion: "reduce",
  });
  await page.addInitScript(clockShim(NOON));
  await page.goto(url, { waitUntil: "networkidle" });
  await page.waitForTimeout(settle);
  if (key) {
    await page.mouse.move(700, 450);
    await page.keyboard.press(key);
    await page.waitForTimeout(4000);
  }
  // The chrome comes off. The card supplies its own typography, and the app's
  // panels shrunk to card size are unreadable furniture. A product screenshot
  // wants the opposite — see `shots.mjs`, which keeps the instruments.
  await hide(page, [...FURNITURE.transient, ...FURNITURE.BARE]);
  await page.screenshot({ path: join(HERE, file), timeout: 120_000, animations: "disabled" });
  console.log("art   ", file);
  await page.close();
}

async function renderCard(browser, which, out) {
  const page = await browser.newPage({ viewport: { width: 1200, height: 630 }, deviceScaleFactor: 1 });
  await page.goto(`http://127.0.0.1:8799/og.html?card=${which}`, { waitUntil: "networkidle" });
  // The art is a background image, so `networkidle` is not proof it has decoded.
  await page.evaluate(() => document.fonts.ready);
  await page.waitForTimeout(1200);
  await page.screenshot({ path: join(PUBLIC, out) });
  console.log("card  ", out);
  await page.close();
}

/**
 * `--cards-only` re-renders the two PNGs from art already on disk.
 *
 * The app pass shoots the running city and the card pass is two seconds, and
 * every iteration on a headline needs only the second. Without the flag, tuning
 * a line of copy costs a whole app pass each time, which is how a card ends up
 * shipped with the first wording anybody tried.
 */
const cardsOnly = process.argv.includes("--cards-only");

const app = cardsOnly ? null : await serve(join(ROOT, "dist"), 5210, { spa: true });
const assets = await serve(HERE, 8799);
const browser = await launch();
try {
  if (!cardsOnly) {
    await shootApp(browser, "http://office.lumbridgecorp.com:5210/", "art-office.png");
    await shootApp(browser, "http://tera.lumbridgecorp.com:5210/", "art-tera.png", { key: "2" });
  }
  await renderCard(browser, "tera", "og-tera.png");
  await renderCard(browser, "office", "og-office.png");
} finally {
  await browser.close();
  app?.close();
  assets.close();
}
