// Fullscreen shader passes used by the compositor: blending, feedback, bloom, grade.
// All noise here is keyed off u_frame rather than any random source, so grain is
// identical between a preview run and an export of the same frame.

export const BLEND_FRAG = `
precision highp float;
uniform sampler2D u_base;
uniform sampler2D u_src;
uniform float u_amount;
uniform int   u_mode;
varying vec2 vUv;

vec3 blendOverlay(vec3 b, vec3 s) {
    return mix(2.0 * b * s, 1.0 - 2.0 * (1.0 - b) * (1.0 - s), step(0.5, b));
}
vec3 blendSoftLight(vec3 b, vec3 s) {
    return mix(2.0 * b * s + b * b * (1.0 - 2.0 * s),
               sqrt(b) * (2.0 * s - 1.0) + 2.0 * b * (1.0 - s),
               step(0.5, s));
}

void main() {
    vec4 base = texture2D(u_base, vUv);
    vec4 src  = texture2D(u_src, vUv);
    float a = src.a * u_amount;
    vec3 result;

    if (u_mode == 1)      result = base.rgb + src.rgb * a;                        // add
    else if (u_mode == 2) result = 1.0 - (1.0 - base.rgb) * (1.0 - src.rgb * a);  // screen
    else if (u_mode == 3) result = mix(base.rgb, base.rgb * src.rgb, a);          // multiply
    else if (u_mode == 4) result = mix(base.rgb, blendOverlay(base.rgb, src.rgb), a);
    else if (u_mode == 5) result = mix(base.rgb, blendSoftLight(base.rgb, src.rgb), a);
    else                  result = mix(base.rgb, src.rgb, a);                     // normal

    gl_FragColor = vec4(result, max(base.a, a));
}
`;

/**
 * Frame feedback. Disproportionately responsible for images looking alive rather
 * than merely animated: the previous frame is re-sampled through a small zoom and
 * rotation, decayed, and added back.
 */
export const FEEDBACK_FRAG = `
precision highp float;
uniform sampler2D u_current;
uniform sampler2D u_history;
uniform float u_decay;
uniform float u_amount;
uniform float u_zoom;
uniform float u_rotate;
uniform float u_aspect;
varying vec2 vUv;

void main() {
    vec3 cur = texture2D(u_current, vUv).rgb;

    vec2 p = (vUv - 0.5);
    p.x *= u_aspect;
    float c = cos(u_rotate), s = sin(u_rotate);
    p = mat2(c, -s, s, c) * p * u_zoom;
    p.x /= u_aspect;
    vec2 warped = p + 0.5;

    vec3 hist = texture2D(u_history, clamp(warped, 0.0, 1.0)).rgb;
    // Decay strictly below 1 keeps the loop convergent; the 10k-frame stability
    // check in tools/ verifies it neither saturates to white nor dies to black.
    vec3 outC = cur + hist * u_decay * u_amount;
    gl_FragColor = vec4(min(outC, vec3(4.0)), 1.0);
}
`;

export const BRIGHT_FRAG = `
precision highp float;
uniform sampler2D u_tex;
uniform float u_threshold;
uniform float u_knee;
varying vec2 vUv;
void main() {
    vec3 c = texture2D(u_tex, vUv).rgb;
    float l = dot(c, vec3(0.2126, 0.7152, 0.0722));
    float k = smoothstep(u_threshold, u_threshold + u_knee, l);
    gl_FragColor = vec4(c * k, 1.0);
}
`;

export const BLUR_FRAG = `
precision highp float;
uniform sampler2D u_tex;
uniform vec2 u_direction;   // texel-space step, already scaled by resolution
varying vec2 vUv;
void main() {
    // 9-tap gaussian, separable.
    vec3 sum = texture2D(u_tex, vUv).rgb * 0.227027;
    vec2 o1 = u_direction * 1.3846153846;
    vec2 o2 = u_direction * 3.2307692308;
    sum += (texture2D(u_tex, vUv + o1).rgb + texture2D(u_tex, vUv - o1).rgb) * 0.3162162162;
    sum += (texture2D(u_tex, vUv + o2).rgb + texture2D(u_tex, vUv - o2).rgb) * 0.0702702703;
    gl_FragColor = vec4(sum, 1.0);
}
`;

export const COMPOSITE_FRAG = `
precision highp float;
uniform sampler2D u_tex;
uniform sampler2D u_bloom;
uniform float u_bloomAmount;
uniform float u_chroma;
uniform float u_grain;
uniform float u_vignette;
uniform float u_contrast;
uniform float u_saturation;
uniform float u_lift;
uniform float u_exposure;
uniform float u_fade;
uniform float u_frame;
uniform vec2  u_resolution;
varying vec2 vUv;

float hash13(vec3 p) {
    p = fract(p * 0.1031);
    p += dot(p, p.yzx + 33.33);
    return fract((p.x + p.y) * p.z);
}

void main() {
    vec2 uv = vUv;
    vec2 dir = (uv - 0.5);

    // Chromatic aberration, radial, resolution-independent.
    vec3 col;
    if (u_chroma > 0.0001) {
        float amt = u_chroma * 0.01;
        col.r = texture2D(u_tex, uv - dir * amt).r;
        col.g = texture2D(u_tex, uv).g;
        col.b = texture2D(u_tex, uv + dir * amt).b;
    } else {
        col = texture2D(u_tex, uv).rgb;
    }

    col += texture2D(u_bloom, uv).rgb * u_bloomAmount;
    col *= u_exposure;

    // Grade: lift, contrast, saturation.
    col += u_lift;
    col = (col - 0.5) * u_contrast + 0.5;
    float l = dot(col, vec3(0.2126, 0.7152, 0.0722));
    col = mix(vec3(l), col, u_saturation);

    // Vignette in normalised space so it matches at any output size.
    float v = 1.0 - u_vignette * dot(dir, dir) * 2.0;
    col *= clamp(v, 0.0, 1.0);

    // Deterministic grain: keyed on frame index, never on a random source.
    if (u_grain > 0.0001) {
        float n = hash13(vec3(floor(uv * u_resolution), u_frame));
        col += (n - 0.5) * u_grain;
    }

    col *= u_fade;
    gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
}
`;

export const COPY_FRAG = `
precision highp float;
uniform sampler2D u_tex;
varying vec2 vUv;
void main() { gl_FragColor = texture2D(u_tex, vUv); }
`;

export const BLEND_MODE_IDS = {
    normal: 0, add: 1, screen: 2, multiply: 3, overlay: 4, softlight: 5,
};
