#!/usr/bin/env node
// Scaffold a new scene: writes the module, registers it, and leaves it in a
// state that already passes every static and GPU gate.
//
// The point is token economy as much as typing. Everything a new scene needs
// besides its shader body is mechanical — the params block, the trait
// declarations that match the helpers actually called, the registry import and
// entry, a reactive mapping that is not on a rate param — and every one of
// those has its own lint rule waiting to reject it. Generating them from a
// template means the only thing left to write is the part that needs judgement.
//
// Usage:
//   npm run new:scene -- "Salt Flat" --family=minimal --traits=shape,camera,space,style
//   npm run new:scene -- "Ink Bleed" --family=organic --traits=camera,style --feedback
//
// Then edit the `scene()` body. The skeleton renders a live, animated, seeded
// field, so the scene is gate-clean from the first run and stays that way while
// you replace the body a piece at a time.

import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';

const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const SCENES = join(root, 'src/scenes/shader');
const REGISTRY = join(root, 'src/scenes/registry.js');

const FAMILIES = ['flow', 'organic', 'minimal', 'structural', 'geometric', 'glitch'];
const TRAITS = ['shape', 'camera', 'space', 'style'];

const args = process.argv.slice(2);
const flags = new Map(args.filter((a) => a.startsWith('--')).map((a) => {
    const [k, v] = a.replace(/^--/, '').split('=');
    return [k, v === undefined ? true : v];
}));
const name = args.find((a) => !a.startsWith('--'));

if (!name) {
    console.error(`usage: npm run new:scene -- "Scene Name" --family=<${FAMILIES.join('|')}> ` +
                  `--traits=<${TRAITS.join(',')}> [--feedback]`);
    process.exit(1);
}

const family = String(flags.get('family') || 'flow');
if (!FAMILIES.includes(family)) {
    console.error(`unknown family '${family}' — one of ${FAMILIES.join(', ')}`);
    process.exit(1);
}

const traits = String(flags.get('traits') || 'camera,style').split(',').map((t) => t.trim()).filter(Boolean);
for (const t of traits) {
    if (!TRAITS.includes(t)) {
        console.error(`unknown trait '${t}' — one of ${TRAITS.join(', ')}`);
        process.exit(1);
    }
}

const kebab = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
const camel = kebab.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
const file = join(SCENES, `${kebab}.js`);

if (existsSync(file)) {
    console.error(`${kebab}.js already exists`);
    process.exit(1);
}

/**
 * A per-scene constant baked into the skeleton.
 *
 * Two freshly scaffolded scenes would otherwise render identically and trip the
 * "no two scenes render the same image" gate before either has been written.
 * Deriving the constants from the name means the skeleton is already distinct.
 */
function salt(text, lo, hi) {
    let h = 2166136261;
    for (let i = 0; i < text.length; i++) {
        h ^= text.charCodeAt(i);
        h = Math.imul(h, 16777619) >>> 0;
    }
    return +(lo + (h % 1000) / 1000 * (hi - lo)).toFixed(2);
}

const freq = salt(kebab, 2.5, 7.5);
const skew = salt(kebab + 'x', 0.4, 2.2);
const warp = salt(kebab + 'w', 0.3, 1.6);

// Trait expression: each declared trait gets a real call, because the lint
// greps for one and the Phase 9 render gate then measures that it changed the
// image. A trait declared and not used fails both, in that order.
const traitLines = {
    camera: '    p = sigCamera(p);',
    space:  '    float horizon = sigHorizonY();\n    p.y -= horizon * 0.3;',
    shape:  '',   // used in the body below
    style:  '',   // used in the body below
};

const head = traits.map((t) => traitLines[t]).filter(Boolean).join('\n');
const shapeLine = traits.includes('shape')
    ? '\n    // TRAIT shape: the track\'s signature form, so this scene is cast from\n' +
      '    // the same actors as every other scene in the video.\n' +
      '    float form = sigShape(p / max(u_size, 1e-3)) * u_size;\n' +
      '    col += pal(3) * sigEdgeOrMask(form);'
    : '';
const styleLine = traits.includes('style')
    ? '\n    // TRAIT style: the track\'s art direction.\n    col += sigGrain(uv);'
    : '';
const spaceLine = traits.includes('space')
    ? '\n    col = sigAir(col, p, smoothstep(0.0, 1.6, length(p)));'
    : '';
const feedbackLine = flags.get('feedback')
    ? '\n    // Feedback. The base field above must stand alone: a scene that only\n' +
      '    // reads prev() is black for its first frames and fragile under seek.\n' +
      '    col = max(col, prev(uv - vec2(0.0, 0.002)) * u_persist);'
    : '';

const shapeHelper = traits.includes('shape')
    ? `
// Fill for the signature form, drawn in the track's line weight.
float sigEdgeOrMask(float d) {
    return smoothstep(0.01, -0.01, d) * 0.6 + sigEdge(d);
}
`
    : '';

const params = [
    `        scale:   { type: 'float', range: [1, 12],      default: ${freq}, uniform: 'u_scale', bias: 'density' },`,
    `        speed:   { type: 'float', range: [0.05, 1.2],  default: 0.3,  uniform: 'u_speed', bias: 'motion', rate: true },`,
    `        detail:  { type: 'float', range: [0.2, 2.5],   default: ${warp}, uniform: 'u_detail', bias: 'density' },`,
    `        glow:    { type: 'float', range: [0, 1.5],     default: 0.5,  uniform: 'u_glow', bias: 'energy' },`,
    traits.includes('shape')
        ? `        size:    { type: 'float', range: [0.05, 0.6],  default: 0.25, uniform: 'u_size' },`
        : null,
    flags.get('feedback')
        ? `        persist: { type: 'float', range: [0, 0.85],    default: 0.4,  uniform: 'u_persist' },`
        : null,
    `        palette: { type: 'palette', count: 5 },`,
].filter(Boolean).join('\n');

const source = `// ${family[0].toUpperCase() + family.slice(1)} family: TODO one line on what this looks like.
//
// TODO: say what makes it DIFFERENT from the scenes it sits next to. That
// sentence is the scene's reason to exist, and "no two scenes render the same
// image" is a gate, not a guideline.
//
// Scaffolded by tools/new-scene.js. See HOWTO-visualizers.md.

export const ${camel} = {
    name: '${name.trim()}',
    family: '${family}',
    kind: 'fragment',
    traits: [${traits.map((t) => `'${t}'`).join(', ')}],

    params: {
${params}
    },

    reactive: {
        glow:   { feature: 'beat',    amount: 0.3, response: 'spike' },
        detail: { feature: 'bandMid', amount: 0.25, response: 'smooth' },
    },

    shader: \`${shapeHelper}
vec4 scene(vec2 uv, vec2 p) {
    float t = u_time * u_speed + u_seed;
${head}

    // TODO: replace this field. It exists so the skeleton is live, animated and
    // distinct from every other scene the moment it is registered.
    float n = fbm(p * u_scale * 0.5 + vec2(t * 0.4, -t * ${skew}), 4);
    float band = sin(n * u_detail * 6.0 + length(p) * ${freq} - t * 2.0) * 0.5 + 0.5;

    vec3 col = mix(pal(0) * 0.08, pal(1), band);
    col += pal(2) * pow(band, 4.0) * u_glow;${shapeLine}${spaceLine}${feedbackLine}${styleLine}

    return vec4(col, 1.0);
}
\`,
};

export default ${camel};
`;

writeFileSync(file, source);

// --- register -------------------------------------------------------------
let registry = readFileSync(REGISTRY, 'utf8');
const importLine = `import { ${camel} } from './shader/${kebab}.js';`;
if (!registry.includes(importLine)) {
    const lastImport = registry.lastIndexOf("} from './shader/");
    const eol = registry.indexOf('\n', lastImport);
    registry = registry.slice(0, eol + 1) + importLine + '\n' + registry.slice(eol + 1);
}
registry = registry.replace(/\n\];/, `\n    ${camel},\n];`);
writeFileSync(REGISTRY, registry);

console.log(`created  src/scenes/shader/${kebab}.js`);
console.log(`registered  ${camel} (${family}, traits: ${traits.join(', ') || 'none'})`);
console.log('');
console.log('next:');
console.log('  1. write the scene() body — everything else is done');
console.log('  2. npm run lint:scenes');
console.log(`  3. open  http://localhost:5180/checks.html?scene=${encodeURIComponent(name.trim())}`);
