// The uniform contract every shader scene is compiled against.
//
// Scenes do not write `main()`. They define:
//
//     vec4 scene(vec2 uv, vec2 p)
//
// where `uv` is 0..1 across the frame and `p` is centred, aspect-corrected,
// roughly -1..1 on the short axis. Everything else — the preamble, the varying,
// main() itself — is injected here. That boilerplate reduction is what makes a
// thirty-scene library affordable to write and to keep consistent.
//
// RESOLUTION INDEPENDENCE: work in `uv`/`p`, never in pixels. If you genuinely
// need a pixel-sized feature, scale it by u_pixelScale so a 720p preview and a
// 4K export agree. The dual-resolution diff in tools/ exists to catch violations.

export const VERTEX_SHADER = `
varying vec2 vUv;
void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

/** Audio-reactive uniforms, filled per frame from the FeatureTrack. */
export const AUDIO_UNIFORMS = [
    'u_loudness',   // overall level, track-normalised 0..1
    'u_rms',
    'u_bandSub',    // 20-60 Hz
    'u_bandLow',    // 60-250 Hz
    'u_bandMid',    // 250-2k
    'u_bandHigh',   // 2k-6k
    'u_bandAir',    // 6k-16k
    'u_flux',       // onset strength
    'u_centroid',   // spectral brightness 0..1
    'u_flatness',   // noisy vs tonal 0..1
    'u_width',      // stereo width 0..1
    'u_beat',       // decaying spike on each beat, 1 at the hit
    'u_beatPhase',  // 0..1 within the beat
    'u_barPhase',   // 0..1 within the bar
    'u_phrasePhase',// 0..1 within an 8-bar phrase
    'u_sectionProgress',
    'u_sectionEnergy',
    'u_buildSlope', // >0 while energy is ramping toward the next section
];

/**
 * The track's personality, constant for the whole video. See look/Personality.js.
 *
 * These are what make sixteen unrelated shaders read as one production. A scene
 * declares in `traits` which of them it honours, and the look generator will not
 * cast a scene that cannot express what the track is built on.
 *
 * All of them are neutral by default, so a layer built without a personality
 * renders exactly what it always rendered.
 */
export const SIGNATURE_UNIFORMS = {
    u_sigSides: 'float',    // signature form: 0 = round, else polygon sides
    u_sigRound: 'float',    // corner rounding of that form
    u_sigElong: 'float',    // how far from square the form is
    u_sigTilt: 'float',     // its resting angle

    u_sigDrift: 'vec2',     // camera translation per second
    u_sigSway: 'float',     // camera sway amplitude
    u_sigSwayRate: 'float',
    u_sigSpin: 'float',     // slow camera roll, radians per second
    u_sigBreathe: 'float',  // bar-locked zoom

    u_sigHorizon: 'float',  // where the ground meets the sky, 0..1 up the frame
    u_sigDepth: 'float',    // distance falloff
    u_sigWash: 'vec2',      // background gradient direction and strength

    u_sigLine: 'float',     // line weight
    u_sigSoft: 'float',     // edge softness
    u_sigTexture: 'float',  // surface grain
    u_sigFold: 'float',     // kaleidoscopic folds, 1 = none

    // FRAMING. Not a personality trait — this one is per SHOT, pushed by the
    // arc driver rather than derived from the track. See look/framing.js.
    //
    // Every scene in the library is a locked-off, full-frame wide, and always
    // has been. That is one shot type, held for the length of a song, and it is
    // the reason cutting between two scenes changes the subject but never the
    // FRAMING — which is at least half of how a real edit holds attention.
    //
    // Applied inside sigCamera, in scene coordinates, so a close-up is rendered
    // close rather than being a magnified 720p image. That distinction is the
    // whole reason this is a coordinate transform and not a post pass.
    u_sigFrameScale: 'float',   // >1 pushes in, <1 pulls back
    u_sigFrameShift: 'vec2',    // recentre, in scene units
};

export const FRAME_UNIFORMS = [
    'u_time', 'u_frame', 'u_progress', 'u_seed',
    'u_resolution', 'u_aspect', 'u_pixelScale', 'u_opacity',
];

export const PREAMBLE = `
precision highp float;

uniform vec2  u_resolution;
uniform float u_aspect;
uniform float u_pixelScale;
uniform float u_time;
uniform float u_frame;
uniform float u_progress;
uniform float u_seed;
uniform float u_opacity;

uniform vec3  u_colors[8];
uniform int   u_colorCount;

${AUDIO_UNIFORMS.map((u) => `uniform float ${u};`).join('\n')}

${Object.entries(SIGNATURE_UNIFORMS).map(([u, t]) => `uniform ${t} ${u};`).join('\n')}

uniform sampler2D u_prev;
uniform int       u_hasPrev;

varying vec2 vUv;

// --- palette helpers -------------------------------------------------------
// Scenes should reach for these instead of hardcoding colours, so the look
// generator can actually recolour them per track.

vec3 pal(int i) {
    int n = max(u_colorCount, 1);
    int k = int(mod(float(i), float(n)));
    for (int j = 0; j < 8; j++) { if (j == k) return u_colors[j]; }
    return u_colors[0];
}

/** Continuous ramp through the palette; t wraps. */
vec3 palRamp(float t) {
    int n = max(u_colorCount, 1);
    float f = fract(t) * float(n);
    int i = int(floor(f));
    return mix(pal(i), pal(i + 1), smoothstep(0.0, 1.0, fract(f)));
}

// --- noise -----------------------------------------------------------------

float hash11(float n) { return fract(sin(n) * 43758.5453123); }
float hash12(vec2 p)  { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453123); }
vec2  hash22(vec2 p)  {
    vec3 a = fract(vec3(p.xyx) * vec3(123.34, 234.34, 345.65));
    a += dot(a, a + 34.45);
    return fract(vec2(a.x * a.y, a.y * a.z));
}

float vnoise(vec2 p) {
    vec2 i = floor(p), f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    float a = hash12(i), b = hash12(i + vec2(1.0, 0.0));
    float c = hash12(i + vec2(0.0, 1.0)), d = hash12(i + vec2(1.0, 1.0));
    return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}

float fbm(vec2 p, int octaves) {
    float v = 0.0, amp = 0.5;
    for (int i = 0; i < 8; i++) {
        if (i >= octaves) break;
        v += amp * vnoise(p);
        p *= 2.02;
        amp *= 0.5;
    }
    return v;
}

/** Cheap divergence-free-ish flow field. The backbone of the "flow" family. */
vec2 curl(vec2 p, float t) {
    float e = 0.1;
    float n1 = fbm(p + vec2(0.0, e) + t, 4);
    float n2 = fbm(p - vec2(0.0, e) + t, 4);
    float n3 = fbm(p + vec2(e, 0.0) + t, 4);
    float n4 = fbm(p - vec2(e, 0.0) + t, 4);
    return vec2(n1 - n2, n4 - n3) / (2.0 * e);
}

mat2 rot(float a) { float c = cos(a), s = sin(a); return mat2(c, -s, s, c); }

/** N-fold kaleidoscopic fold of a centred coordinate. */
vec2 kaleido(vec2 p, float sides) {
    if (sides < 1.5) return p;
    float a = atan(p.y, p.x);
    float r = length(p);
    float seg = 6.28318530718 / sides;
    a = abs(mod(a + seg * 0.5, seg) - seg * 0.5);
    return vec2(cos(a), sin(a)) * r;
}

// --- personality -----------------------------------------------------------
// The four traits, as functions a scene applies in its own way. A scene that
// calls none of these must not declare the matching trait, or it will be cast
// in a track it cannot express. See look/Personality.js.

/**
 * TRAIT: shape. Signed distance to the track's signature form, radius ~1.
 * Round tracks return a circle, so a scene can call this unconditionally.
 */
float sigShape(vec2 q) {
    q = rot(u_sigTilt) * q;
    q.x /= max(u_sigElong, 0.05);

    if (u_sigSides < 2.5) return length(q) - 1.0;

    // Regular polygon by angular folding, then rounded back toward the circle.
    float seg = 6.28318530718 / u_sigSides;
    float a = atan(q.y, q.x);
    float r = length(q);
    float folded = cos(mod(a + seg * 0.5, seg) - seg * 0.5);
    float poly = r * folded - cos(seg * 0.5);
    return mix(poly, r - 1.0, clamp(u_sigRound, 0.0, 1.0));
}

/** TRAIT: shape, as a filled mask of the given radius, centred on a point. */
float sigForm(vec2 p, vec2 centre, float size) {
    float d = sigShape((p - centre) / max(size, 1e-3)) * max(size, 1e-3);
    return smoothstep(u_sigSoft * 0.25 + 0.004, -u_sigSoft * 0.25, d);
}

/**
 * TRAIT: camera. The same operator filming every scene — a slow drift, a sway,
 * a roll, and a bar-locked breath. Apply to a centred coordinate before using it.
 */
vec2 sigCamera(vec2 p) {
    float t = u_time;
    p = rot(u_sigSpin * t) * p;
    p *= 1.0 - u_sigBreathe * sin(u_barPhase * 6.28318530718);
    p += vec2(sin(t * u_sigSwayRate), cos(t * u_sigSwayRate * 0.83)) * u_sigSway;
    // The pan RETURNS. A constant translation would be a camera on rails: two
    // minutes in, every scene has left the frame entirely. This is a slow track
    // across the subject and back, roughly a two-minute cycle.
    p -= u_sigDrift * 20.0 * sin(t * 0.05);
    return p;
}

/** TRAIT: style. Fold the frame the track's way. 1 fold means no fold. */
vec2 sigFolded(vec2 p) {
    return kaleido(p, u_sigFold);
}

/** TRAIT: style. Turn a signed distance into an edge drawn in the track's hand. */
float sigEdge(float d) {
    float w = 0.004 + u_sigLine * 0.03;
    float soft = w * (0.25 + u_sigSoft * 1.5);
    return smoothstep(w + soft, w - soft, abs(d));
}

/** TRAIT: style. The track's surface grain, for a scene to add at its own weight. */
float sigGrain(vec2 uv) {
    if (u_sigTexture <= 0.001) return 0.0;
    return (hash12(uv * 512.0 + floor(u_frame)) - 0.5) * u_sigTexture;
}

/**
 * TRAIT: space. Height of the shared horizon in the same units as 'p'.
 * Positive is up; a scene with any sense of ground should sit on it.
 */
float sigHorizonY() {
    return (u_sigHorizon - 0.5) * 2.0;
}

/** TRAIT: space. The location's air: distance haze plus the background wash. */
vec3 sigAir(vec3 col, vec2 p, float distance01) {
    vec3 far = pal(0) * (0.25 + 0.35 * u_sigDepth);
    col = mix(col, far, clamp(distance01, 0.0, 1.0) * u_sigDepth);
    col += pal(1) * dot(p, u_sigWash) * 0.35;
    return col;
}

vec3 prev(vec2 uv) {
    if (u_hasPrev == 0) return vec3(0.0);
    return texture2D(u_prev, uv).rgb;
}

float sat(float x) { return clamp(x, 0.0, 1.0); }
vec3  sat3(vec3 x) { return clamp(x, 0.0, 1.0); }

/**
 * The framed equivalent of uv, for scenes that build their image in uv space.
 *
 * Screen-space scenes — a scan tear, a vertical transposition — slice the FRAME
 * and are right to do so: a signal artefact happens to the signal, not to the
 * world behind it. But the imagery behind the slicing is still a subject, and a
 * subject can be filmed wide or close. So the slice grid stays on raw uv while
 * the field it displaces is built from this, which carries the shot's framing.
 */
vec2 framedUv(vec2 p) { return vec2(p.x / u_aspect, p.y) * 0.5 + 0.5; }
`;

const EPILOGUE = `
void main() {
    vec2 uv = vUv;
    vec2 p = (uv - 0.5) * 2.0;
    p.x *= u_aspect;

    // FRAMING is applied here, to the coordinate every scene is handed, rather
    // than inside sigCamera where it started out.
    //
    // sigCamera is gated on the camera personality trait: a scene that does
    // not want the track's drift and sway simply never calls it. That is
    // correct for a TRAIT and wrong for framing, which is not one — it is where
    // the camera is standing for this shot, and no scene should be exempt from
    // it because of an unrelated art-direction decision. Measured, that
    // accident left Scan Tear and Pylon Grid completely unframed, and the two
    // are otherwise perfectly good candidates for a close-up.
    //
    // uv is deliberately NOT framed. It is screen space: prev() reads the
    // feedback buffer with it and sigGrain speckles in it, and both of those
    // belong to the output image rather than to the scene being filmed.
    p = p / max(u_sigFrameScale, 0.05) + u_sigFrameShift;

    vec4 col = scene(uv, p);
    gl_FragColor = vec4(col.rgb, col.a * u_opacity);
}
`;

/**
 * Assemble a complete fragment shader from a scene body plus its declared params.
 * Param uniforms are appended to the preamble so a scene never declares them itself —
 * the schema is the single source of truth, which is what the lint checks.
 */
export function buildFragmentShader(sceneModule) {
    const paramUniforms = [];
    for (const [name, def] of Object.entries(sceneModule.params || {})) {
        if (!def.uniform) continue;
        if (def.type === 'palette') continue; // palette rides in u_colors
        const glslType = def.type === 'int' ? 'int' : def.type === 'vec2' ? 'vec2' : 'float';
        paramUniforms.push(`uniform ${glslType} ${def.uniform}; // param: ${name}`);
    }

    return [
        PREAMBLE,
        paramUniforms.join('\n'),
        '\n// ---- scene ----\n',
        sceneModule.shader,
        EPILOGUE,
    ].join('\n');
}
