#version 330 core
// Phase 3i -- Overlay-family transition between clip A's frame and a
// solid overlay color, modulated by a per-variant alpha curve. Four
// `TransitionType` entries share this shader, distinguished by an
// integer `u_variant` selector:
//
//   u_variant = 0  Black  -- triangular alpha envelope over solid
//                  black. `alpha(p) = 1 - |2p - 1|` peaks at p=0.5
//                  (full black) and returns 0 at p=0 / p=1.
//                  Symmetric fade-to-black-and-back across the
//                  window. Maps to macOS HTML §5.1 Black cell.
//
//   u_variant = 1  White  -- same triangular envelope over solid
//                  white. Maps to macOS HTML §5.1 White cell.
//
//   u_variant = 2  Light  -- gaussian spike over solid white;
//                  narrower than the triangular curve so the
//                  flash reads as a "blink of light" rather than
//                  a long wash. `alpha(p) = exp(-0.5 * ((p - 0.5)
//                  / 0.12)^2)` -- peak 1 at p=0.5, ~0.05 at the
//                  window edges. Maps to macOS HTML §5.1
//                  Floodlight cell.
//
//   u_variant = 3  Blink  -- 3-pulse rectangular train over solid
//                  black, centres at 1/6, 3/6, 5/6, half-width
//                  0.03 each. Each pulse is a discrete strobe.
//                  Maps to macOS HTML §5.1 Blink cell.
//
// Math port verification (P3i byte-equal contract)
// ------------------------------------------------
// The CPU oracle `sampleTransitionCurve` in
// `src/render/TransitionCurve.cpp` holds the canonical alpha-curve
// formulas. This shader's `alphaForVariant(...)` helper ports those
// formulas verbatim into GLSL: same constants (sigma 0.12, 3-pulse
// centres, half-width 0.03), same formulas (`1 - |2p - 1|`,
// `exp(-0.5 * dx^2)`, rectangular train), same clamp on `p`. The
// CPU oracle is intentionally retained post-cutover so a future
// regression on this shader can be diff'd against the CPU output
// at sample progress points (see the `test_transition_shader_overlay`
// helper plus `transition-pipeline.md §20.4` byte-equal section).
//
// `u_progress` is normalised to `[0, 1]` across the full transition
// window (Phase 2 `transitionPhaseProgress` contract). There is no
// `u_direction` and no `u_softness` -- overlay families are
// omnidirectional and the curve constants are baked into the
// shader.
//
// B-clip uniforms intentionally absent
// ------------------------------------
// Unlike every other dual-clip shader (Wipe / Slide / Push /
// Dissolve / Blur / Pixelate / Circle / Zoom / Rotate / Shake)
// the Overlay family does NOT need a second YUV clip. The legacy
// single-clip overlay path painted a 1×1 RGBA premul colour over
// the canvas-base; the consolidated dual-clip path samples the
// canvas-base frame as A inside the fitted viewport and outputs
// `mix(aRgb, overlayColor, alpha)` directly. `bYuv` aliases to
// `aYuv` in the calling code so the existing
// `dualClipFrame.bYuv->valid()` gate continues to hold without a
// special-case relax; the shader itself never references B
// uniforms or B samplers.
//
// Per-clip Orientation (mirror / flip / rotation) is applied to A's
// UV before the YUV sample so a Black overlay on a flipped clip
// behaves identically to the legacy single-clip overlay step (the
// legacy path drew videos[0] through `drawYuvFullscreen` which
// applied orient internally, then painted a uniform 1×1 colour on
// top -- the orient on the underlying clip is preserved end-to-end).

out vec4 FragColor;
in  vec2 TexCoord;

uniform sampler2D tex_aY;
uniform sampler2D tex_aU;
uniform sampler2D tex_aV;

uniform float u_progress;
uniform int   u_variant;

uniform int   u_aFlipX;
uniform int   u_aFlipY;
uniform float u_aRotRad;

// Per-clip Orientation. Verbatim copy of the helper in every
// other dual-clip shader so an overlay over a rotated A behaves
// identically to a wipe / dissolve over the same rotated A.
vec2 applyOrient(vec2 uv, int flipX, int flipY, float rotRad) {
    if (rotRad != 0.0) {
        vec2 c = uv - vec2(0.5);
        float s  = sin(rotRad);
        float co = cos(rotRad);
        uv = vec2(co * c.x - s * c.y, s * c.x + co * c.y) + vec2(0.5);
    }
    if (flipX != 0) uv.x = 1.0 - uv.x;
    if (flipY != 0) uv.y = 1.0 - uv.y;
    return uv;
}

// BT.709 limited-range YUV -> RGB. Identical coefficients to every
// other dual-clip shader so the overlay boundary does not shift
// colours relative to the single-clip path on either side of the
// transition.
vec3 yuvToRgb(float y, float u, float v) {
    y = (y - 16.0/255.0)  * (255.0/219.0);
    u = (u - 128.0/255.0) * (255.0/224.0);
    v = (v - 128.0/255.0) * (255.0/224.0);
    float r = y + 1.5748 * v;
    float g = y - 0.1873 * u - 0.4681 * v;
    float b = y + 1.8556 * u;
    return clamp(vec3(r, g, b), 0.0, 1.0);
}

vec3 sampleClip(sampler2D yT, sampler2D uT, sampler2D vT, vec2 uv) {
    // OOR -> letterbox black. Matches every other dual-clip shader.
    if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
        return vec3(0.0);
    }
    return yuvToRgb(texture(yT, uv).r,
                    texture(uT, uv).r,
                    texture(vT, uv).r);
}

// Per-variant alpha curve. Direct GLSL port of `TransitionCurve.cpp`
// `triangleEnvelope` / `gaussianSpike` / `pulseTrain`; constants
// kept identical so the CPU oracle and GPU port agree to within
// 1/255 8-bit quantisation (the only divergence source is float32
// vs float64 evaluation of the same formulas, which lands well
// below the 8-bit step at all eight sample progress points
// {0, 0.125, 0.25, ..., 1.0}).
float alphaForVariant(int variant, float p) {
    if (variant == 0 || variant == 1) {
        // Black (0) and White (1) share the triangular envelope.
        return 1.0 - abs(2.0 * p - 1.0);
    }
    if (variant == 2) {
        // Light: gaussian spike at p=0.5, sigma 0.12.
        float dx = (p - 0.5) / 0.12;
        return exp(-0.5 * dx * dx);
    }
    if (variant == 3) {
        // Blink: "eye close -> hold -> eye open" envelope. Trapezoidal
        // shape -- alpha smoothsteps 0->1 in the first 25% (closing),
        // holds at full black for the middle 50% (eyes shut), then
        // smoothsteps 1->0 in the last 25% (opening). Replaces the
        // earlier 3-pulse strobe which read as a flicker rather than
        // a single blink.
        float close = smoothstep(0.0, 0.25, p);
        float open  = 1.0 - smoothstep(0.75, 1.0, p);
        return min(close, open);
    }
    return 0.0;
}

// Per-variant overlay colour. Black/Blink paint solid black,
// White/Light paint solid white. Matches `TransitionCurve.cpp`
// `buildPremul(.., r, g, b)` constants: Black (0,0,0), White
// (255,255,255), Light (255,255,255), Blink (0,0,0). Light and
// Blink use the SAME RGB as their luminance siblings; the
// distinction is the alpha curve, not the colour.
vec3 colorForVariant(int variant) {
    if (variant == 1 || variant == 2) {
        return vec3(1.0);
    }
    return vec3(0.0);
}

void main() {
    // Defensive clamp -- the CPU side already clamps `progress`
    // through `transitionPhaseProgress`, but doubling up here
    // matches every other dual-clip shader and avoids a NaN /
    // Inf uniform from triggering driver-specific behaviour.
    float p = clamp(u_progress, 0.0, 1.0);

    // Variant clamp -- {0, 1, 2, 3}. Out-of-range falls back to 0
    // (Black) which is the conservative default. The
    // GpuRasterPrimitive's `safeVariant` clamp already pins this
    // server-side; the in-shader clamp is the second belt-and-
    // braces.
    int variant = clamp(u_variant, 0, 3);

    vec2 uvA = applyOrient(TexCoord, u_aFlipX, u_aFlipY, u_aRotRad);
    vec3 aRgb = sampleClip(tex_aY, tex_aU, tex_aV, uvA);

    vec3 rgb;
    if (variant == 2) {
        // ----- Light (Floodlight) -------------------------------
        // Android-aligned single-shader approximation of the
        // 5-pass `EditorImageFloodlightFilterGroup` chain:
        //   1. Highlight extract = smoothstep(thresh - 0.15, thresh,
        //      brightness). Threshold scales with cover progress so
        //      midpoint extracts the most pixels.
        //   2-3. Gaussian blur on the highlight image.
        //   4. Select + merge: result = original + highlight * 5 *
        //      sin(cp * pi/2).
        //   5. Final gaussian blur on the composite.
        //
        // Compressed into one fragment by computing the blurred
        // highlight (and blurred original) directly from a 5x5
        // gaussian-weighted neighbourhood -- avoids the 5-pass
        // FBO ping-pong at the cost of 25 texture taps per
        // fragment.
        const float kCoverPi2  = 1.5707963267948966;
        // coverPercent: symmetric triangle 0 -> 1 (at p=0.5) -> 0.
        float cp = (p < 0.5) ? (p * 2.0) : ((1.0 - p) * 2.0);
        // Highlight extract threshold: clamp(1 - easeInCubic(cp), 0.2, 1).
        // easeInCubic(x) = 1 - cos(x * pi/2).
        float threshold = clamp(cos(cp * kCoverPi2), 0.2, 1.0);
        // Blur radius scales with cp (zero blur at the window edges).
        const float kMaxBlurUV = 0.045;
        float blurStep = kMaxBlurUV * cp;

        vec3 highlightBlur = vec3(0.0);
        vec3 originalBlur  = vec3(0.0);
        float totalW = 0.0;
        const int kHalf = 2;     // 5x5 = 25 samples
        for (int dy = -kHalf; dy <= kHalf; ++dy) {
            for (int dx = -kHalf; dx <= kHalf; ++dx) {
                vec2 offset = vec2(float(dx), float(dy)) *
                              (blurStep / float(kHalf));
                vec3 c = sampleClip(tex_aY, tex_aU, tex_aV, uvA + offset);
                vec3 hl = smoothstep(vec3(threshold - 0.15),
                                     vec3(threshold), c);
                float r2 = float(dx * dx + dy * dy);
                float w  = exp(-r2 * 0.5 /
                               (float(kHalf) * float(kHalf)));
                highlightBlur += hl * w;
                originalBlur  += c  * w;
                totalW += w;
            }
        }
        highlightBlur /= max(totalW, 0.001);
        originalBlur  /= max(totalW, 0.001);

        // Select+merge ease: sin(cp * pi/2) -- 0 at endpoints,
        // 1 at midpoint, smooth.
        float selectEase = sin(cp * kCoverPi2);
        rgb = clamp(originalBlur + highlightBlur * 5.0 * selectEase,
                    0.0, 1.0);
    } else if (variant == 3) {
        // ----- Blink: curved bidirectional curtain ---------------
        // Two black eyelids sweep in from the top/bottom edges, meeting
        // in a horizontal-lens (eye) shape: the opening is tallest at
        // the centre column and tapers to the L/R corners (bows
        // OUTWARD). The lids reach FULL closure with a brief hold at
        // the midpoint before reopening, matching the Blink icon.
        const float kPi = 3.14159265358979323846;
        // closeAmt overshoots 0.5 around the midpoint so the lids fully
        // meet (and hold) at p = 0.5 instead of leaving a centre slit.
        float closeAmt = (0.5 - abs(p - 0.5)) * 1.15;
        float dx  = 2.0 * TexCoord.x - 1.0;
        float arc = 1.0 - dx * dx;          // 1 at centre, 0 at edges
        // Eye-shape curve recesses the lids away from the centre so the
        // opening is tallest there; the sin envelope fades it to zero
        // at both fully-open and fully-closed, so the open state has no
        // stray edge bars and the closed state is solid black.
        float curveEnv = sin(clamp(closeAmt / 0.5, 0.0, 1.0) * kPi);
        float effDepth = closeAmt + 0.12 * (1.0 - arc) * curveEnv;
        float band = 0.02;
        float distToEdge = min(TexCoord.y, 1.0 - TexCoord.y);
        float inBar = 1.0 - smoothstep(effDepth - band, effDepth + band, distToEdge);
        // The canvas-base layer (aRgb here) is whichever clip covers
        // tUs -- A in the OUT half, B in the IN half. Painting black
        // inside the bars gives the eye-blink close-and-reopen feel
        // with a clean A->B swap behind the closed curtain.
        rgb = mix(aRgb, vec3(0.0), inBar);
    } else {
        // Black / White: simple linear alpha overlay.
        float alpha = alphaForVariant(variant, p);
        vec3  ovr   = colorForVariant(variant);
        rgb = mix(aRgb, ovr, alpha);
    }
    FragColor = vec4(rgb, 1.0);
}
