// 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
];

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

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')}

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;
}

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); }
`;

const EPILOGUE = `
void main() {
    vec2 uv = vUv;
    vec2 p = (uv - 0.5) * 2.0;
    p.x *= u_aspect;
    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');
}
