#!/usr/bin/env node
/**
 * Offscreen mesh review.
 *
 *   pnpm preview                          # bare male + female base bodies
 *   pnpm preview -- --wire                # with topology overlay
 *   pnpm preview -- --clay                # flat clay instead of skin palette
 *   pnpm preview -- --id survivor         # an assembled preset
 *   pnpm preview -- --body female --style mass=0.6,muscle=-0.2,fat=0.3
 *   pnpm preview -- --body male --face length=-0.4,jaw=0.8,brow=0.6
 *   pnpm preview -- --backfaces           # magenta = inverted winding
 */
import { mkdir, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { rasterToPng, type Raster } from "../image/Raster.js";
import { computeFlatNormals } from "../mesh/MeshBuilder.js";
import { buildSkeleton } from "../rig/skeleton.js";
import { assembleCharacter } from "../creator/assemble.js";
import { buildBaseBody } from "../creator/body.js";
import { normalizeBodyStyle, type BodyStyle } from "../creator/bodyStyle.js";
import { normalizeHeadStyle, type HeadStyle } from "../creator/headStyle.js";
import { DEFAULT_HEIGHT, measurementsFor } from "../creator/proportions.js";
import { presetById } from "../creator/presets.js";
import type { BodyType, CreatorPalette } from "../creator/types.js";
import { buildDefaultClips } from "../anim/procedural.js";
import { poseMesh } from "./pose.js";
import { renderSheet, stackRasters, type RenderOptions } from "./render.js";

const ROOT = resolve(fileURLToPath(new URL("../../..", import.meta.url)));
const OUT_DIR = join(ROOT, "assets", "generated", "preview");

const NEUTRAL_PALETTE: CreatorPalette = {
  skin: { r: 200, g: 160, b: 130 },
  hair: { r: 60, g: 45, b: 38 },
  primary: { r: 90, g: 96, b: 110 },
  secondary: { r: 60, g: 64, b: 74 },
  accent: { r: 150, g: 60, b: 50 },
  metal: { r: 150, g: 152, b: 158 },
  leather: { r: 80, g: 58, b: 42 },
};

const VIEWS = [
  { yaw: 0 },
  { yaw: 35 },
  { yaw: 90 },
  { yaw: 180 },
];

interface Args {
  id?: string;
  body?: BodyType;
  style: Partial<BodyStyle>;
  face: Partial<HeadStyle>;
  wire: boolean;
  clay: boolean;
  backfaces: boolean;
  size: number;
  out?: string;
  clip?: string;
  /** Phases through the clip to render, 0..1. */
  phases: number[];
  help: boolean;
}

function parseArgs(argv: string[]): Args {
  const args: Args = { style: {}, face: {}, wire: false, clay: false, backfaces: false, size: 420, phases: [], help: false };
  for (let i = 0; i < argv.length; i++) {
    const token = argv[i]!;
    if (token === "--") continue;
    if (token === "--id" || token === "-i") args.id = argv[++i];
    else if (token === "--body" || token === "-b") args.body = argv[++i] as BodyType;
    else if (token === "--wire" || token === "-w") args.wire = true;
    else if (token === "--clay") args.clay = true;
    else if (token === "--backfaces") args.backfaces = true;
    else if (token === "--size" || token === "-s") args.size = Number(argv[++i]);
    else if (token === "--out" || token === "-o") args.out = argv[++i];
    else if (token === "--style" || token === "--face") {
      const bag = token === "--face" ? args.face : args.style;
      for (const pair of (argv[++i] ?? "").split(",")) {
        const [key, value] = pair.split("=");
        if (key && value !== undefined) {
          (bag as Record<string, number>)[key.trim()] = Number(value);
        }
      }
    } else if (token === "--clip" || token === "-c") args.clip = argv[++i];
    else if (token === "--phase" || token === "-p") {
      args.phases = (argv[++i] ?? "").split(",").map(Number).filter(Number.isFinite);
    } else if (token === "--help" || token === "-h") args.help = true;
  }
  return args;
}

function baseBody(body: BodyType, style: BodyStyle, face: HeadStyle) {
  const skeleton = buildSkeleton(measurementsFor(body), { height: DEFAULT_HEIGHT[body] });
  const mesh = buildBaseBody(skeleton, body, NEUTRAL_PALETTE, style, face);
  computeFlatNormals(mesh);
  return mesh;
}

async function main(): Promise<void> {
  const args = parseArgs(process.argv.slice(2));
  if (args.help) {
    console.log(
      [
        "pnpm preview                       bare male + female base bodies",
        "pnpm preview -- --wire             topology overlay",
        "pnpm preview -- --clay             flat clay shading",
        "pnpm preview -- --backfaces        magenta = inverted winding",
        "pnpm preview -- --id survivor      assembled preset",
        "pnpm preview -- --body female --style mass=0.5,fat=0.3",
        "pnpm preview -- --body male --face length=-0.4,jaw=0.8,brow=0.6",
        "pnpm preview -- --id survivor --clip Walk        four phases of a clip",
        "pnpm preview -- --id survivor --clip Walk --phase 0.25,0.5",
        "pnpm preview -- --size 520 --out /tmp/base.png",
      ].join("\n"),
    );
    return;
  }

  const options: RenderOptions = {
    size: args.size,
    wireframe: args.wire,
    showBackfaces: args.backfaces,
    ...(args.clay ? { clay: { r: 186, g: 182, b: 176 } } : {}),
  };

  const rows: Raster[] = [];
  let label: string;

  if (args.id) {
    const recipe = presetById(args.id);
    if (!recipe) throw new Error(`Unknown preset "${args.id}"`);
    const assembled = assembleCharacter(recipe);

    if (args.clip) {
      // Deformation, not silhouette: one view, several moments in the cycle.
      const clip = buildDefaultClips().find(
        (c) => c.name.toLowerCase() === args.clip!.toLowerCase(),
      );
      if (!clip) {
        throw new Error(
          `Unknown clip "${args.clip}". Have: ${buildDefaultClips().map((c) => c.name).join(", ")}`,
        );
      }
      const phases = args.phases.length > 0 ? args.phases : [0, 0.25, 0.5, 0.75];
      for (const yaw of [90, 35]) {
        rows.push(
          renderSheet(
            phases.map((phase) => ({
              mesh: poseMesh(assembled.mesh, assembled.skeleton, clip, phase * clip.duration),
              yaw,
            })),
            options,
          ),
        );
      }
      label = `${recipe.id}-${clip.name.toLowerCase()}`;
    } else {
      rows.push(renderSheet(VIEWS.map((v) => ({ mesh: assembled.mesh, ...v })), options));
      label = recipe.id;
    }
  } else {
    const style = normalizeBodyStyle(args.style);
    const face = normalizeHeadStyle(args.face);
    const bodies: BodyType[] = args.body ? [args.body] : ["male", "female"];
    for (const body of bodies) {
      const mesh = baseBody(body, style, face);
      console.log(
        `${body.padEnd(7)} ${mesh.positions.length / 3} vertices, ${mesh.indices.length / 3} triangles`,
      );
      rows.push(renderSheet(VIEWS.map((v) => ({ mesh, ...v })), options));
    }
    label = args.body ? `base-${args.body}` : "base-body";
  }

  const sheet = stackRasters(rows);
  const outPath = args.out ?? join(OUT_DIR, `${label}.png`);
  await mkdir(resolve(outPath, ".."), { recursive: true });
  await writeFile(outPath, rasterToPng(sheet));
  console.log(`${outPath}  ${sheet.width}x${sheet.height}`);
}

main().catch((error: unknown) => {
  console.error(error instanceof Error ? error.message : String(error));
  process.exit(1);
});
