/**
 * Photograph the surface library.
 *
 *   node scripts/brand-assets/textures.mjs
 *
 * `/simulate/assets` on lumbridgecorp.com is a page whose whole argument is that
 * the art in this repository is code — eight texture kinds drawn on a 2D canvas
 * from seeded value noise, thirty-five surface roles that pair one of those with
 * a colour and a roughness, and no binary art anywhere, enforced by a CI job.
 * It made that argument in about four thousand pixels of prose with nothing on
 * it to look at, which is a strange way to talk about the appearance of things.
 *
 * So this writes the library out: one tile per texture kind, drawn by
 * `TextureBin`, multiplied by the colour of the first role that carries it, and
 * one neutral tile beside it so a reader can see for themselves that the map has
 * no hue of its own. The role table goes with them as data, so the site can set
 * it in its own type rather than baking labels into an image.
 *
 * ### Why this one runs against Vite dev and the others run against `dist/`
 *
 * `shots.mjs` and `films.mjs` photograph the *application*, and the application
 * a visitor gets is the built bundle, so that is what they point a camera at.
 * This photographs a *module*. `TextureBin` is not reachable from the built
 * bundle — it exposes no names — and making it reachable would mean a third Vite
 * entry, which would ship a texture-sheet page to tera.lumbridgecorp.com so that
 * a script could take a screenshot of it. Vite dev serves and transforms the
 * source on request, so the page imports the same files a reader opens on the
 * repository. That is a shorter chain of custody than the bundle, not a longer
 * one, and the manifest records the commit either way.
 *
 * ### It needs no GPU
 *
 * Nothing here draws with WebGL. The textures are Canvas2D and the encoder is
 * the browser's own WebP, so this runs in a couple of seconds on the software
 * path and there is nothing to check a renderer string for. Chrome is still the
 * right tool: `toDataURL("image/webp")` is the encoder, exactly as in
 * `shots.mjs`, because there is no `sharp` on this box.
 *
 * ### Flags
 *
 *   --site <dir>   where lumbridge-v4 is (default ../../lumbridge-v4)
 *   --manifest-only  rewrite the generated TS without drawing anything
 */

import { chromium } from "playwright";
import { createServer } from "vite";
import { mkdir, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { execFileSync } from "node:child_process";

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

const args = process.argv.slice(2);
function flag(name, fallback = null) {
  const i = args.indexOf(name);
  return i === -1 ? fallback : args[i + 1];
}
const SITE = flag("--site", join(ROOT, "..", "lumbridge-v4"));
const MANIFEST_ONLY = args.includes("--manifest-only");

const PUBLIC_DIR = join(SITE, "apps", "web", "public", "textures");
const DATA_DIR = join(SITE, "apps", "web", "src", "data");

/**
 * A sentence per texture kind, written here rather than on the website.
 *
 * Same rule the shot captions follow: the description lives next to the thing
 * that produces it, so a texture that is rewritten cannot leave a paragraph
 * behind on a marketing page describing the grain it used to have. The website
 * renders these; it does not author them.
 *
 * Every claim below is checkable against `DRAW` in `src/assets/textures.ts`.
 */
const NOTES = {
  carpetLoop:
    "Two octaves of value noise for the pile, then a loop pattern punched over it on a half-offset grid — the same trick a real loop carpet uses to hide its seams.",
  woodPlank:
    "Planks of a random width, each with its own grain running the length of it, and a hairline gap where two meet.",
  polishedConcrete:
    "Broad, slow noise for the pour, a fine speckle for the aggregate, and a few pale trowel sweeps that stop it reading as a gradient.",
  ceilingTile:
    "A mineral-fibre face: dense fine noise, pinholes punched through it, and a chamfer drawn around the edge of the tile.",
  plasterPaint:
    "Almost nothing, on purpose. Roller texture at the threshold of visibility is what keeps a painted wall from reading as a flat fill under raking light.",
  fabricWeave:
    "A warp and a weft drawn as alternating lines, which is enough structure to catch a highlight without becoming a pattern anybody notices.",
  tileGrid:
    "A hard grid of grout lines over lightly varied tiles, so no two squares in a floor are quite the same value.",
  whiteboard:
    "A near-white gloss with a faint vertical wipe and a few ghosted horizontal strokes — a board that has been used and cleaned, not a board out of a box.",
};

function sha() {
  try {
    return execFileSync("git", ["rev-parse", "--short", "HEAD"], { cwd: ROOT })
      .toString()
      .trim();
  } catch {
    return "unknown";
  }
}

function dirty() {
  try {
    return (
      execFileSync("git", ["status", "--porcelain"], { cwd: ROOT }).toString().trim() !== ""
    );
  } catch {
    return true;
  }
}

async function draw() {
  // `server.middlewareMode` is off: the page is fetched over HTTP like any
  // other, so Vite's HTML transform runs and the inline module script is
  // rewritten with resolved import URLs.
  const server = await createServer({
    root: ROOT,
    logLevel: "warn",
    server: { port: 5313, strictPort: true },
  });
  await server.listen();

  const browser = await chromium.launch({ channel: "chrome" });
  try {
    const page = await browser.newPage();
    const errors = [];
    page.on("pageerror", (error) => errors.push(String(error)));
    await page.goto("http://localhost:5313/scripts/brand-assets/textures.html", {
      waitUntil: "networkidle",
    });
    // The tiles are drawn during module evaluation, so this is the whole wait.
    // `waitForFunction`, not `waitForSelector`: the page paints nothing, so its
    // body has no height, and Playwright's visibility rule counts a zero-height
    // element as hidden however present it is.
    await page.waitForFunction(() => document.body.dataset.ready === "1", null, {
      timeout: 30000,
    });
    if (errors.length) throw new Error(`page errors: ${errors.join("; ")}`);

    const library = await page.evaluate(() => window.__library());
    if (!library.textures.length) throw new Error("no textures came back");
    return library;
  } finally {
    await browser.close();
    await server.close();
  }
}

function manifest(library) {
  const rows = library.textures
    .map(
      (t) => `  {
    kind: ${JSON.stringify(t.kind)},
    src: ${JSON.stringify(`/textures/${t.kind}.webp`)},
    neutralSrc: ${JSON.stringify(`/textures/${t.kind}-neutral.webp`)},
    size: ${t.size},
    leadRole: ${JSON.stringify(t.leadRole)},
    leadColor: ${JSON.stringify(t.leadColor)},
    carriedBy: ${JSON.stringify(t.carriedBy)},
    note: ${JSON.stringify(NOTES[t.kind] ?? "")},
  },`,
    )
    .join("\n");

  const surfaces = library.surfaces
    .map(
      (s) =>
        `  { role: ${JSON.stringify(s.role)}, color: ${JSON.stringify(s.color)}, texture: ${JSON.stringify(s.texture)}, roughness: ${s.roughness}, metalness: ${s.metalness} },`,
    )
    .join("\n");

  return `/**
 * Generated. Do not edit — \`scripts/brand-assets/textures.mjs\` in the tera repo
 * rewrites this file wholesale, and the descriptions below live next to the code
 * that draws each texture so the two cannot drift apart.
 *
 * Every tile named here was drawn by tera's own \`TextureBin\` at the commit
 * recorded below, and every row in \`SURFACES\` is what \`MaterialRegistry\` hands
 * the office for that role. Nothing is an illustration and nothing is retouched.
 *
 * To change a description: edit \`NOTES\` in that script and run it with
 * \`--manifest-only\`, which redraws nothing.
 */

/** A union rather than \`string\`, so a page naming a texture that is gone fails typecheck. */
export type TextureKind =
${library.textures.map((t) => `  | ${JSON.stringify(t.kind)}`).join("\n")};

export interface TextureTile {
  kind: TextureKind;
  /** The map multiplied by \`leadColor\` — what the office actually renders. */
  src: string;
  /** The map alone. Near-white by construction: the hue arrives from the palette. */
  neutralSrc: string;
  /** Pixels per side, which is also metres-per-repeat divided into resolution. */
  size: number;
  /** The first role in palette order that carries this texture. */
  leadRole: string;
  leadColor: string;
  /** Every role that carries it. */
  carriedBy: string[];
  note: string;
}

/** One row per surface role, as \`MaterialRegistry\` builds it at \`high\` quality. */
export interface Surface {
  role: string;
  color: string;
  texture: string | null;
  roughness: number | null;
  metalness: number | null;
}

/** The tera commit these were drawn from. */
export const TEXTURES_COMMIT = ${JSON.stringify(sha())};
/** True if that commit is not the whole story — the tree had uncommitted work. */
export const TEXTURES_DIRTY = ${dirty()};

export const TEXTURES: TextureTile[] = [
${rows}
];

export const SURFACES: Surface[] = [
${surfaces}
];

/** How many metres one repeat covers. From \`TEXTURE_TILE_METRES\` in tera. */
export const TEXTURE_TILE_METRES = 2;
`;
}

const dataUrl = (value) => Buffer.from(value.split(",")[1], "base64");

async function main() {
  if (MANIFEST_ONLY) {
    // Redrawing is the only way to know the role table, so a manifest-only run
    // still needs the page — it just does not write any images.
    const library = await draw();
    await writeFile(join(DATA_DIR, "textures.generated.ts"), manifest(library));
    console.log(`manifest only → ${join(DATA_DIR, "textures.generated.ts")}`);
    return;
  }

  const library = await draw();
  await mkdir(PUBLIC_DIR, { recursive: true });

  let bytes = 0;
  for (const tile of library.textures) {
    const tinted = dataUrl(tile.tinted);
    const neutral = dataUrl(tile.neutral);
    await writeFile(join(PUBLIC_DIR, `${tile.kind}.webp`), tinted);
    await writeFile(join(PUBLIC_DIR, `${tile.kind}-neutral.webp`), neutral);
    bytes += tinted.length + neutral.length;
    console.log(
      `${tile.kind.padEnd(18)} ${tile.size}px  ${tile.leadColor}  ${tile.carriedBy.length} role(s)  ${Math.round((tinted.length + neutral.length) / 1024)} kB`,
    );
  }

  await writeFile(join(DATA_DIR, "textures.generated.ts"), manifest(library));
  console.log(
    `\n${library.textures.length} textures, ${library.surfaces.length} surface roles, ${Math.round(bytes / 1024)} kB total → ${SITE}`,
  );
  if (dirty()) console.log("warning: tera's tree is dirty; the manifest names a dirty sha");
}

await main();
