#version 450 core

// Unified backdrop blur fragment shader.
// Handles both the 1D separable blur passes (mode 0, used for BOTH the H-pass and V-pass;
// `direction` picks the axis) and the composite pass (mode 1, reads the fully-blurred
// working texture, masks via RRect SDF, applies tint, and writes to source_texture with
// premultiplied-over blending). Working textures are sized at the full swapchain resolution;
// downsampled content occupies only a sub-rect at downsample factor > 1 (set via viewport).
//
// The composite blends with source_texture via the standard premultiplied-over blend state
// (ONE, ONE_MINUS_SRC_ALPHA).
//
// Backdrop primitives are tint-only — there is no outline. A specialized edge effect
// (e.g. liquid-glass-style refraction outlines) would be implemented as a dedicated
// primitive type with its own pipeline.
//
// Two modes, structurally distinct:
//
//   Mode 0: 1D separable blur. Used for BOTH the H-pass and V-pass; `direction` (set in the
//           per-pass uniforms) picks (1,0) for H or (0,1) for V. Reads the previous working-
//           res texture and writes the next working-res texture. Fullscreen-triangle vertex
//           output; gl_FragCoord.xy is in working-res target pixel space; UV =
//           gl_FragCoord.xy * inv_working_size.
//
//   Mode 1: composite. Reads the fully-blurred working-res texture, applies the SDF mask and
//           tint, writes to source_texture. Instanced unit-quad vertex output covering the
//           per-primitive bounds; gl_FragCoord.xy is in the full-resolution render target;
//           UV into the blurred working texture =
//           (gl_FragCoord.xy * inv_downsample_factor) * inv_working_size.
//           No kernel is applied here — the blur is already complete.
//
// V-blur is run as its own working→working pass rather than folded into the composite. The
// folded variant produced a horizontal-vs-vertical asymmetry artifact: when V-blur sampled
// the H-blur output through the bilinear-upsample/SDF-mask/tint pipeline in one shader
// invocation, horizontal source features ended up looking sharper than vertical ones.
// Matching V's structure exactly to H's restores symmetry.

const uint MAX_KERNEL_PAIRS = 32;

// --- Inputs from vertex shader ---
layout(location = 0) in vec2 p_local;
layout(location = 1) in mediump vec4 f_color;
layout(location = 2) flat in vec2 f_half_size_ppx;
layout(location = 3) flat in vec4 f_radii_ppx;
layout(location = 4) flat in float f_half_feather_ppx;

// --- Output ---
layout(location = 0) out vec4 out_color;

// --- Sampler ---
// Mode 0: bound to downsample_texture. Mode 1: bound to h_blur_texture.
layout(set = 2, binding = 0) uniform sampler2D blur_input_tex;

// --- Uniforms (set 3) ---
// Per-bracket-substage. `mode` matches the vertex shader's mode (0 = H, 1 = V).
// `direction` selects the kernel axis for blur offsets.
// `kernel` holds the per-sigma weight/offset pairs computed CPU-side using the
// linear-sampling pair adjustment (RAD/Rákos).
layout(set = 3, binding = 0) uniform Uniforms {
    vec2 inv_working_size; // 1.0 / working-resolution texture dimensions
    uint pair_count; // number of (weight, offset) pairs; pair[0] is the center
    uint mode; // 0 = H-blur, 1 = V-composite
    vec2 direction; // (1,0) for H, (0,1) for V — multiplied into the kernel offset
    float inv_downsample_factor; // 1.0 / downsample_factor (mode 1 only; mode 0 ignores)
    float _pad0;
    vec4 kernel[MAX_KERNEL_PAIRS]; // .x = weight (paired-sum for idx>0), .y = offset (texels)
};

// ---------------------------------------------------------------------------------------------------------------------
// ----- SDF helper --------------------
// ---------------------------------------------------------------------------------------------------------------------

float sdRoundedBox(vec2 p, vec2 b, vec4 r) {
    vec2 rxy = (p.x > 0.0) ? r.xy : r.zw;
    float rr = (p.y > 0.0) ? rxy.x : rxy.y;
    vec2 q = abs(p) - b;
    if (rr == 0.0) {
        return max(q.x, q.y);
    }
    q += rr;
    return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr;
}

float sdf_alpha(float d, float h) {
    return 1.0 - smoothstep(-h, h, d);
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Blur sample loop --------------
// ---------------------------------------------------------------------------------------------------------------------

vec3 blur_sample(vec2 uv) {
    vec3 color = kernel[0].x * texture(blur_input_tex, uv).rgb;

    // Per-pair offset in texel space, projected onto the active axis.
    vec2 axis_step = direction * inv_working_size;

    for (uint i = 1u; i < pair_count; i += 1u) {
        float w = kernel[i].x;
        float off = kernel[i].y;
        vec2 step_uv = off * axis_step;
        color += w * texture(blur_input_tex, uv - step_uv).rgb;
        color += w * texture(blur_input_tex, uv + step_uv).rgb;
    }

    return color;
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Main --------------------------
// ---------------------------------------------------------------------------------------------------------------------

void main() {
    if (mode == 0u) {
        // ---- Mode 0: 1D separable blur (used for both H-pass and V-pass).
        // gl_FragCoord is in working-res target pixel space; sample the previous working-res
        // texture along `direction` with the kernel.
        vec2 uv = gl_FragCoord.xy * inv_working_size;
        vec3 color = blur_sample(uv);
        out_color = vec4(color, 1.0);
        return;
    }

    // ---- Mode 1: composite per-primitive.
    // RRect SDF — early discard for fragments well outside the masked region.
    float d = sdRoundedBox(p_local, f_half_size_ppx, f_radii_ppx);
    if (d > f_half_feather_ppx) {
        discard;
    }

    // fwidth-based normalization for AA (matches main pipeline approach).
    float grad_magnitude = max(fwidth(d), 1e-6);
    float d_n = d / grad_magnitude;
    float h_n = f_half_feather_ppx / grad_magnitude;

    // Sample the fully-blurred working-res texture. gl_FragCoord is full-res; convert to
    // working-res UV via inv_downsample_factor. No kernel is applied — the H+V blur passes
    // already produced the final blurred image; this is just an upsample + tint.
    vec2 uv = (gl_FragCoord.xy * inv_downsample_factor) * inv_working_size;
    vec3 color = texture(blur_input_tex, uv).rgb;

    // Tint composition: inside the masked region the panel is fully opaque — it completely
    // hides the original framebuffer content, just like real frosted glass and like iOS
    // UIBlurEffect / CSS backdrop-filter. f_color.rgb specifies the tint color; f_color.a
    // specifies the tint *mix strength* (NOT panel opacity). At alpha=0 we see the pure
    // blur; at alpha=255 we see the blur fully multiplied by the tint color.
    //
    // Output is premultiplied to match the ONE, ONE_MINUS_SRC_ALPHA blend state. Coverage
    // (the SDF mask's edge AA) modulates only the alpha channel, never the panel-vs-source
    // blend; that way edge pixels still feather correctly while mid-panel pixels stay fully
    // opaque.
    mediump vec3 tinted = mix(color, color * f_color.rgb, f_color.a);
    mediump float coverage = sdf_alpha(d_n, h_n);
    out_color = vec4(tinted * coverage, coverage);
}
