// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! GLES 2/3 shader sources used by `GlesCanvas`. All fragment shaders
//! emit premultiplied-alpha colour so they compose correctly under
//! the pipeline's default `glBlendFunc(ONE, ONE_MINUS_SRC_ALPHA)`.
//!
//! Every constant is `pub(super)` so only files inside
//! `crate::gles_render` can reach them — callers always go through
//! `GlesCanvas`'s public methods, not the raw shader source.

// Vertex shader shared by both programs (just transforms a unit quad).
// GLSL ES 1.00 — works on both GLES2 and GLES3 contexts (forward compatible
// when no `#version` directive is present).
pub( super ) const VERT_SRC: &str = r#"
attribute vec2 a_pos;
varying vec2 v_uv;
uniform mat4 u_mvp;
void main()
{
	v_uv = a_pos;
	gl_Position = u_mvp * vec4(a_pos, 0.0, 1.0);
}
"#;

// Fragment shader for solid/rounded rects (signed-distance smoothstep at edge).
//
// `u_stroke == 0` ⇒ filled rect. The shader fills the rounded shape with
// `u_color`, antialiased over a 1-pixel band at the edge.
// `u_stroke > 0`  ⇒ stroked outline of width `u_stroke`, centered on the
// rounded boundary, so outlines keep their corner radius.
//
// Distance formula is Iñigo Quílez's exact SDF for a rounded box, extended
// to per-corner radii by picking `r` per-quadrant:
//   r = u_radii[ corner_index_from_sign( p - center ) ]
//   q = abs(p - center) - (size/2 - r)
//   d = min(max(q.x, q.y), 0) + length(max(q, 0)) - r
// `u_radii` is ordered `(tl, tr, br, bl)` clockwise from top-left and is
// uploaded as `Corners::to_uniform()`. A uniform-radii fill is the
// degenerate case where every component is equal — the per-quadrant
// branch collapses to the single-`r` formula.
//
// `u_pad` lets the caller draw the quad larger than `u_size` (used by
// `stroke_rect`, which expands the quad by `stroke/2` on each side so the
// outer half of the stroke has fragments to cover). The shader maps `v_uv`
// from the larger quad back into the original-rect space:
//   p = v_uv * (size + 2*pad) - pad
// so `p` ranges `[-pad, size+pad]`. Keeping `u_size` and `u_radii` at the
// *original* values is essential — expanding them shifts the SDF zero-line
// outward and breaks the circle case (radius = size/2).
//
// Each component of `u_radii` is clamped to `min(size.x, size.y) * 0.5`
// before use. Callers frequently pass very large values (e.g.
// `theme::RADIUS = 100`) as a "please make this a pill / capsule"
// sentinel. Without the clamp, `size/2 - r` goes negative in the shorter
// dimension and the rounded-box formula degenerates — rendering an
// ellipse in the middle of the rect instead of a pill.
//
// Emits premultiplied-alpha colour. The pipeline blend is
// `(ONE, ONE_MINUS_SRC_ALPHA)`, so every shader that writes colour must
// premultiply its RGB by the final alpha. This matters for non-opaque
// fills (coverage from the SDF, translucent `u_color.a`) where straight-
// alpha output would under-saturate antialiased edges.
pub( super ) const RECT_FRAG_SRC: &str = r##"
precision mediump float;
varying vec2 v_uv;
uniform vec4 u_color;
uniform vec2 u_size;
uniform vec4 u_radii;
uniform float u_stroke;
uniform float u_pad;

// Per-fragment corner radius lookup. `c` is the fragment position
// relative to the rect's centre. Inside the shader, p.y grows UPWARD:
// `ortho_rect` flips the v_uv axis so v_uv.y=0 maps to the bottom edge
// of the rect in screen space and v_uv.y=1 to the top. So with `c =
// p - size/2`:
//   c.x < 0, c.y > 0 → top-left  → r.x
//   c.x > 0, c.y > 0 → top-right → r.y
//   c.x > 0, c.y < 0 → bottom-right → r.z
//   c.x < 0, c.y < 0 → bottom-left  → r.w
float corner_radius(vec2 c, vec4 r)
{
	float top    = (c.x < 0.0) ? r.x : r.y;
	float bottom = (c.x < 0.0) ? r.w : r.z;
	return (c.y > 0.0) ? top : bottom;
}

void main()
{
	float r_max = max(max(u_radii.x, u_radii.y), max(u_radii.z, u_radii.w));
	if (r_max <= 0.0 && u_stroke <= 0.0 && u_pad <= 0.0)
	{
		float a = u_color.a;
		gl_FragColor = vec4(u_color.rgb * a, a);
		return;
	}
	vec2 p = v_uv * (u_size + 2.0 * vec2(u_pad)) - vec2(u_pad);
	vec2 c = p - u_size * 0.5;
	float r = corner_radius(c, u_radii);
	r = min(r, min(u_size.x, u_size.y) * 0.5);
	vec2 q = abs(c) - (u_size * 0.5 - vec2(r));
	float d = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
	// 2-px-wide AA band (half-width 1.0). A narrower (±0.5) band is
	// only sampled when pixel centres happen to fall inside it — when
	// the rasterizer grid aligns with the edge at subpixel ±0.5 the
	// band is jumped over and the transition becomes a binary step
	// (visible stair-stepping on curved edges). Doubling the band
	// guarantees at least one partial-coverage sample per scanline
	// regardless of alignment. The quad pad set by the caller is 1 px
	// so this still fits inside the geometry at d ≤ 1.
	float coverage;
	if (u_stroke > 0.0)
	{
		float half_w = u_stroke * 0.5;
		coverage = 1.0 - smoothstep(half_w - 1.0, half_w + 1.0, abs(d));
	} else {
		coverage = 1.0 - smoothstep(-1.0, 1.0, d);
	}
	float a = u_color.a * coverage;
	gl_FragColor = vec4(u_color.rgb * a, a);
}
"##;

// Fragment shader for RGBA textured quads (images / icons).
//
// Flips uv.y because images are uploaded top-down (CPU convention: row 0 =
// top of source) but `glTexImage2D` puts the first row at the texture's
// lower-left. With the quad orientation produced by `ortho_rect`, sampling
// `v_uv` directly would draw the source upside-down on screen.
//
// Texture data arrives PREMULTIPLIED — `upload_rgba_texture` premuls the
// straight-alpha CPU buffer once at upload. GL_LINEAR sampling can then
// blend across texel boundaries without halo artefacts (a fully opaque
// black texel next to a fully transparent white texel interpolates to
// `( 0, 0, 0, 0.5 )` instead of the halo-producing `( 0.5, 0.5, 0.5, 0.5 )`
// of straight-alpha interpolation). Multiplying premul by uniform opacity
// preserves the invariant `rgb == rgb_straight * a`.
pub( super ) const TEX_FRAG_SRC: &str = r##"
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_sampler;
uniform float u_opacity;
void main()
{
	gl_FragColor = texture2D(u_sampler, vec2(v_uv.x, 1.0 - v_uv.y)) * u_opacity;
}
"##;

// Clip-layer composite shader for `set_clip_path`. Path-clipped content is
// drawn to an offscreen layer; this composites that layer onto the canvas
// multiplied by an anti-aliased coverage mask, giving a smooth clipped edge.
// `u_layer` is the full-canvas layer texture (premultiplied), `u_mask` the
// path coverage rasterised over `u_bbox` (x, y, w, h in canvas pixels). The
// quad is drawn over the bbox; `v_uv` runs 0..1 across it. Both FBO-backed
// textures are sampled y-flipped (GL's lower-left origin).
pub( super ) const CLIP_COMPOSITE_FRAG_SRC: &str = r##"
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_layer;
uniform sampler2D u_mask;
uniform vec2 u_canvas;
uniform vec4 u_bbox;
void main()
{
	// `v_uv.y` runs bottom-to-top in screen space (FBO origin is lower-left, as
	// `ortho_rect` + the texture shader's flip establish), so the screen Y of
	// this fragment is `bbox.y + (1 - v_uv.y) * bbox.h`.
	vec2 sp  = vec2( u_bbox.x + v_uv.x * u_bbox.z, u_bbox.y + ( 1.0 - v_uv.y ) * u_bbox.w );
	vec2 luv = vec2( sp.x / u_canvas.x, 1.0 - sp.y / u_canvas.y );
	vec4 col = texture2D( u_layer, luv );
	float cov = texture2D( u_mask, vec2( v_uv.x, 1.0 - v_uv.y ) ).a;
	gl_FragColor = col * cov;
}
"##;

// Fragment shader for single-channel glyph textures with color tint.
//
// Glyphs live in a shared GL_LUMINANCE atlas. `u_uv_scale` and `u_uv_offset`
// map the unit quad's (0,0)–(1,1) UV range into the glyph's sub-region:
//   tex_uv = vec2(v_uv.x, 1 - v_uv.y) * u_uv_scale + u_uv_offset
// The Y-flip (`1 - v_uv.y`) corrects for fontdue bitmaps being stored
// top-row-first while GL tex coords have V=0 at the bottom.
//
// The atlas is GL_R8 on ES3 and GL_LUMINANCE on ES2 (see
// `gles_render/text.rs` module doc and `GlesCanvas::atlas_format`).
// Both formats put the coverage byte in `.r` when sampled, so this
// shader is identical for both.
pub( super ) const GLYPH_FRAG_SRC: &str = r##"
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_sampler;
uniform vec4 u_color;
uniform float u_opacity;
uniform vec2 u_uv_offset;
uniform vec2 u_uv_scale;
void main()
{
	vec2 uv = vec2(v_uv.x, 1.0 - v_uv.y) * u_uv_scale + u_uv_offset;
	float coverage = texture2D(u_sampler, uv).r;
	float a = u_color.a * coverage * u_opacity;
	gl_FragColor = vec4(u_color.rgb * a, a);
}
"##;

// Batched glyph shader. `a_pos` is pre-transformed into NDC by the
// CPU side (we know the surface size) so the shader has no MVP to
// apply — that lets us upload one VBO and draw every glyph of the
// text run in a single `glDrawArrays`. `a_uv` carries the per-vertex
// atlas coordinate ready to sample.
pub( super ) const GLYPH_BATCH_VERT_SRC: &str = r#"
attribute vec2 a_pos;
attribute vec2 a_uv;
varying vec2 v_uv;
void main()
{
	v_uv = a_uv;
	gl_Position = vec4(a_pos, 0.0, 1.0);
}
"#;

pub( super ) const GLYPH_BATCH_FRAG_SRC: &str = r##"
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_sampler;
uniform vec4 u_color;
uniform float u_opacity;
void main()
{
	float coverage = texture2D(u_sampler, v_uv).r;
	float a = u_color.a * coverage * u_opacity;
	gl_FragColor = vec4(u_color.rgb * a, a);
}
"##;

// Vertex shader for the present-blit: maps the unit quad to fullscreen NDC
// without going through an MVP. UVs are equal to a_pos so the FBO appears
// the same way up on the default framebuffer as it was rendered into the FBO
// (both use GL pixel coords with origin at bottom-left).
pub( super ) const BLIT_VERT_SRC: &str = r#"
attribute vec2 a_pos;
varying vec2 v_uv;
void main()
{
	v_uv = a_pos;
	gl_Position = vec4(a_pos * 2.0 - 1.0, 0.0, 1.0);
}
"#;

// Fragment shader for the present-blit: straight texture sample, no opacity.
pub( super ) const BLIT_FRAG_SRC: &str = r#"
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_sampler;
void main()
{
	gl_FragColor = texture2D(u_sampler, v_uv);
}
"#;

// Fragment shader for inter-FBO blits (used by `blit` to composite a sub-canvas
// back onto its parent). Reuses `VERT_SRC` so the quad is positioned via
// `u_mvp` like any other textured draw. No Y-flip: source and destination are
// both FBOs storing content in GL's native bottom-up convention, so sampling
// at `v_uv` puts the visually-top row of the source on the top of the dest.
//
// `u_fade_bottom_px` feathers the bottom edge of the blit: the last
// `u_fade_bottom_px` visible rows ramp the source alpha linearly from 1.0
// (at the inner edge of the band) to 0.0 (at the very bottom row), so a
// growing viewport does not look like a knife cut against whatever is
// behind it. `v_uv.y == 1.0` is the visually-top row of the source (FBO
// origin is bottom-left), so `v_uv.y * u_height_px` is the distance in
// source pixels from the bottom; the linear ramp falls out of dividing
// that by `u_fade_bottom_px` and clamping. With `u_fade_bottom_px == 0`
// the divide is skipped and the blit stays hard-edged. Linear (not
// smoothstep) because premultiplied output multiplied by a linear alpha
// already reads as a soft fade — smoothstep would compress the ramp into
// fewer effective rows and reintroduce a faint shoulder.
pub( super ) const SUB_BLIT_FRAG_SRC: &str = r#"
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_sampler;
uniform float u_opacity;
uniform float u_fade_bottom_px;
uniform float u_height_px;
void main()
{
	vec4 c = texture2D(u_sampler, v_uv);
	float fade = 1.0;
	if (u_fade_bottom_px > 0.0)
	{
		float y_from_bottom = v_uv.y * u_height_px;
		fade = clamp(y_from_bottom / u_fade_bottom_px, 0.0, 1.0);
	}
	gl_FragColor = c * (u_opacity * fade);
}
"#;

// Linear gradient shader. Samples a CPU-baked 1D LUT (`u_lut`, size
// `gradient_lut::LUT_SAMPLES × 1`, RGBA8, straight-alpha) along the
// CSS linear-gradient convention: the gradient line passes through the
// centre of the rect, `0°` points up, positive angles rotate clockwise.
// Stop positions outside `[0, 1]` are already baked into the LUT via
// linear extrapolation, so the shader only remaps `t` from the extended
// domain `[u_lut_domain_min, u_lut_domain_min + u_lut_domain_span]`
// into the texture's `[0, 1]` sampling range.
//
// The same SDF as `RECT_FRAG_SRC` is reused for the rounded-rect / pill
// silhouette; the coverage multiplies the fragment's alpha before
// premultiplying.
pub( super ) const LINEAR_GRADIENT_FRAG_SRC: &str = r##"
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_lut;
uniform vec2  u_dir;
uniform vec2  u_size;
uniform float u_line_length;
uniform vec4  u_radii;
uniform float u_pad;
uniform float u_lut_domain_min;
uniform float u_lut_domain_span;

// Per-fragment corner radius lookup — see RECT_FRAG_SRC for the
// quadrant convention.
float corner_radius(vec2 c, vec4 r)
{
	float top    = (c.x < 0.0) ? r.x : r.y;
	float bottom = (c.x < 0.0) ? r.w : r.z;
	return (c.y > 0.0) ? top : bottom;
}

void main()
{
	vec2 p = v_uv * (u_size + 2.0 * vec2(u_pad)) - vec2(u_pad);
	vec2 c = p - u_size * 0.5;
	float r = corner_radius(c, u_radii);
	r = min(r, min(u_size.x, u_size.y) * 0.5);
	vec2 q = abs(c) - (u_size * 0.5 - vec2(r));
	float d = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
	// 2-px AA band — see RECT_FRAG_SRC for the grid-alignment rationale.
	float coverage = 1.0 - smoothstep(-1.0, 1.0, d);

	// Gradient evaluated in rect-local pixel space. `p` already accounts
	// for `u_pad` (set by the caller to expand the quad for AA), so the
	// gradient direction stays anchored to the original rect even when
	// the quad spills outside.
	float dist = dot(c, u_dir);
	float t = 0.5 + dist / u_line_length;

	float t_lut = (t - u_lut_domain_min) / u_lut_domain_span;
	vec4 grad = texture2D(u_lut, vec2(clamp(t_lut, 0.0, 1.0), 0.5));

	float a = grad.a * coverage;
	gl_FragColor = vec4(grad.rgb * a, a);
}
"##;

// Radial gradient shader. `u_center` is in box-relative fractions
// (`[0, 1]` on each axis), `u_radius_frac` is the radial extent in the
// same fractional space. `t = distance(v_uv, u_center) / u_radius_frac`
// so stops at `position == 1.0` fall exactly at the chosen radius.
pub( super ) const RADIAL_GRADIENT_FRAG_SRC: &str = r##"
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_lut;
uniform vec2  u_center;
uniform float u_radius_frac;
uniform vec2  u_size;
uniform vec4  u_radii;
uniform float u_pad;
uniform float u_lut_domain_min;
uniform float u_lut_domain_span;

// Per-fragment corner radius lookup — see RECT_FRAG_SRC for the
// quadrant convention.
float corner_radius(vec2 c, vec4 r)
{
	float top    = (c.x < 0.0) ? r.x : r.y;
	float bottom = (c.x < 0.0) ? r.w : r.z;
	return (c.y > 0.0) ? top : bottom;
}

void main()
{
	vec2 p = v_uv * (u_size + 2.0 * vec2(u_pad)) - vec2(u_pad);
	vec2 c = p - u_size * 0.5;
	float r = corner_radius(c, u_radii);
	r = min(r, min(u_size.x, u_size.y) * 0.5);
	vec2 q = abs(c) - (u_size * 0.5 - vec2(r));
	float d = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
	// 2-px AA band — see RECT_FRAG_SRC for the grid-alignment rationale.
	float coverage = 1.0 - smoothstep(-1.0, 1.0, d);

	// Centre/extent in box-relative fractions evaluated against `p`
	// (which already accounts for `u_pad`), so the radial centre stays
	// anchored to the original rect when the quad spills outside.
	vec2 t_uv = p / u_size;
	float t = distance(t_uv, u_center) / max(u_radius_frac, 1e-6);
	float t_lut = (t - u_lut_domain_min) / u_lut_domain_span;
	vec4 grad = texture2D(u_lut, vec2(clamp(t_lut, 0.0, 1.0), 0.5));

	float a = grad.a * coverage;
	gl_FragColor = vec4(grad.rgb * a, a);
}
"##;

// Outer drop shadow shader.
//
// Rather than baking a blurred shape into an intermediate texture via a
// separable Gaussian pass, this uses the analytical soft-shadow
// approximation `exp(-d² / (2σ²))` over the rounded-rect SDF. The maths:
//
//   • `d` is the signed distance from the fragment to the (possibly
//     spread-expanded) shape. `d < 0` is inside, `d > 0` is outside.
//   • `intensity = 1.0` inside the shape, `exp(-d²/2σ²)` outside.
//   • `σ = shadow.blur / 2` (CSS blur radius → Gaussian sigma).
//
// The result is visually indistinguishable from a true Gaussian blur for
// small to moderate σ. For very large σ the tail of `exp()` departs from
// a real Gaussian, at which point a real two-pass blur or a correction
// factor would be needed.
//
// Running the shadow as a single analytic pass means there's no need for
// a per-shadow FBO, no ping-pong, and no framebuffer readback: it is all
// one cheap draw call.
// Inner (inset) shadow shader.
//
// Two SDFs cooperate here:
//
//   • `d_outer` — the signed distance to the surface itself. Positive
//     outside, negative inside. We multiply the final intensity by the
//     smooth-step coverage of this SDF so the inset never leaks past the
//     shape's own silhouette.
//
//   • `d_inner` — the signed distance to the shape shifted by
//     `shadow.offset` and eroded by `shadow.spread`. This is the "hole"
//     the inset falls into: `d_inner ≥ 0` means the pixel is on the
//     shadow side of the offset edge (full intensity); `d_inner < 0`
//     means the pixel is deeper into the unshadowed interior, where the
//     intensity decays as `exp(-d_inner² / (2σ²))`.
//
// Together they reproduce the CSS `inset` semantics: the shadow sits
// inside the rounded rect, biased toward the side opposite `offset`,
// and fades toward the middle with a Gaussian falloff. Premul output
// matches the rest of the pipeline.
pub( super ) const SHADOW_INSET_FRAG_SRC: &str = r##"
precision mediump float;
varying vec2 v_uv;
uniform vec2  u_size;
uniform vec2  u_padding;
uniform vec4  u_radii;
uniform float u_spread;
uniform float u_sigma;
uniform vec2  u_offset;
uniform vec4  u_color;

// Per-fragment corner radius lookup — see RECT_FRAG_SRC for the
// quadrant convention.
float corner_radius(vec2 c, vec4 r)
{
	float top    = (c.x < 0.0) ? r.x : r.y;
	float bottom = (c.x < 0.0) ? r.w : r.z;
	return (c.y > 0.0) ? top : bottom;
}

void main()
{
	vec2 p = v_uv * (u_size + 2.0 * u_padding) - u_padding;
	vec2 c = p - u_size * 0.5;

	// Outer SDF: the shape itself, untransformed. Used to clip the inset
	// to the silhouette. 2-px AA band — see RECT_FRAG_SRC.
	vec2 half_outer = u_size * 0.5;
	float r_outer = corner_radius(c, u_radii);
	r_outer = min(r_outer, min(half_outer.x, half_outer.y));
	vec2 q_outer = abs(c) - (half_outer - vec2(r_outer));
	float d_outer = min(max(q_outer.x, q_outer.y), 0.0) + length(max(q_outer, 0.0)) - r_outer;
	float outer_coverage = 1.0 - smoothstep(-1.0, 1.0, d_outer);

	// Inner SDF: the shape shifted by offset and eroded by spread. Its
	// distance drives the Gaussian falloff. The inner radii match the
	// outer per-corner shape, eroded by `u_spread` (clamped at zero).
	vec2 p_shifted = p - u_offset;
	vec2 c_shifted = p_shifted - u_size * 0.5;
	vec2 half_inner = half_outer - vec2(u_spread);
	// Degenerate case: spread larger than half the shape erodes it to a
	// point. Guard so `half_inner` never goes negative.
	half_inner = max(half_inner, vec2(1e-3));
	vec4 inner_radii = max(u_radii - vec4(u_spread), vec4(0.0));
	float r_inner = corner_radius(c_shifted, inner_radii);
	r_inner = min(r_inner, min(half_inner.x, half_inner.y));
	vec2 q_inner = abs(c_shifted) - (half_inner - vec2(r_inner));
	float d_inner = min(max(q_inner.x, q_inner.y), 0.0) + length(max(q_inner, 0.0)) - r_inner;

	float intensity;
	if (d_inner >= 0.0)
	{
		intensity = 1.0;
	}
	else
	{
		float s = max(u_sigma, 0.5);
		intensity = exp(-(d_inner * d_inner) / (2.0 * s * s));
	}
	intensity *= outer_coverage;

	float a = u_color.a * intensity;
	gl_FragColor = vec4(u_color.rgb * a, a);
}
"##;

pub( super ) const SHADOW_OUTER_FRAG_SRC: &str = r##"
precision mediump float;
varying vec2 v_uv;
uniform vec2  u_size;
uniform vec2  u_padding;
uniform vec4  u_radii;
uniform float u_spread;
uniform float u_sigma;
uniform vec4  u_color;

// Per-fragment corner radius lookup — see RECT_FRAG_SRC for the
// quadrant convention.
float corner_radius(vec2 c, vec4 r)
{
	float top    = (c.x < 0.0) ? r.x : r.y;
	float bottom = (c.x < 0.0) ? r.w : r.z;
	return (c.y > 0.0) ? top : bottom;
}

void main()
{
	vec2 p = v_uv * (u_size + 2.0 * u_padding) - u_padding;
	vec2 c = p - u_size * 0.5;
	vec2 half_sz = u_size * 0.5 + vec2(u_spread);
	// Per-corner shadow radius — outer shape grown uniformly by spread.
	vec4 r_base = max(u_radii + vec4(u_spread), vec4(0.0));
	float r = corner_radius(c, r_base);
	r = min(r, min(half_sz.x, half_sz.y));
	vec2 q = abs(c) - (half_sz - vec2(r));
	float d = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;

	float intensity;
	if (d <= 0.0)
	{
		intensity = 1.0;
	}
	else
	{
		float s = max(u_sigma, 0.5);
		intensity = exp(-(d * d) / (2.0 * s * s));
	}

	float a = u_color.a * intensity;
	gl_FragColor = vec4(u_color.rgb * a, a);
}
"##;

// Horizontal Gaussian blur for the backdrop pipeline. Part one of the
// separable-Gaussian pair: samples the aux_a snapshot horizontally and
// writes to aux_b at the fragment's pixel position. Drawn over a
// vertically-extended surface rect so the vertical pass — which reads
// aux_b up to ±3σ away from each target pixel — has valid data wherever
// it samples. `u_canvas_size` maps `gl_FragCoord` into the aux texture,
// and the aux pair is sized to the main FBO so that mapping is trivial.
//
// Kernel: 41 taps (`RADIUS = 20`), weights computed inline as
// `exp(-i²/(2σ²))` and normalized by the sum. At σ ≈ 11 this captures
// ~93 % of the Gaussian mass — the remainder is a faint tail that is
// visually imperceptible. For σ past ~15 the kernel radius would need
// to grow or a downsample pass would be required; everything else in
// the pipeline already deals in σ and would not change.
pub( super ) const BACKDROP_BLUR_H_FRAG_SRC: &str = r#"
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_source;
uniform vec2 u_texel;
uniform vec2 u_canvas_size;
uniform float u_sigma;
void main()
{
	const int RADIUS = 20;
	vec2 uv = gl_FragCoord.xy / u_canvas_size;
	vec4 total = vec4(0.0);
	float total_w = 0.0;
	for (int i = -RADIUS; i <= RADIUS; i++)
	{
		float fi = float(i);
		float w = exp(-(fi * fi) / (2.0 * u_sigma * u_sigma));
		total += texture2D(u_source, uv + fi * u_texel) * w;
		total_w += w;
	}
	gl_FragColor = total / total_w;
}
"#;

// Vertical Gaussian blur + SDF clip + tint, the last pass of the
// backdrop pipeline. Runs on a quad matching the surface rect and
// writes to the main FBO.
//
// Per fragment: (1) computes the rounded-rect SDF coverage just like
// the rect shader so the backdrop is clipped to the surface shape with
// a 1-pixel anti-aliased edge; (2) samples `u_source` (the H-blurred
// aux_b) vertically with the same 41-tap Gaussian as the H pass; (3)
// applies the optional `u_tint` over the blurred sample using standard
// premul-over math; (4) outputs premultiplied with `alpha = coverage`.
//
// Alpha handling. The output is `(result_rgb * coverage, coverage)` —
// not `(result_rgb * coverage * result_a, coverage * result_a)`. The
// snapshot holds premultiplied content that is effectively opaque
// inside the canvas (every pixel has been written by some draw, even
// if that draw was `clear(0,0,0,0)`; the FBO is never sampled outside
// the canvas bounds). Treating the blurred sample's alpha as 1.0 at
// output time means the composite pass REPLACES the base pixel inside
// the surface shape with the tinted blurred content, rather than
// alpha-blending on top of it. That is the correct semantics for
// `backdrop-filter`: you want the original content to disappear
// entirely where the surface covers it, replaced by the blurred
// version.
//
// `fill_backdrop` runs this before outer shadows / fill / insets so
// later passes composite on top of the blurred backdrop.
pub( super ) const BACKDROP_COMPOSITE_FRAG_SRC: &str = r#"
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_source;
uniform vec2 u_canvas_size;
uniform vec2 u_texel;
uniform float u_sigma;
uniform vec2 u_size;
uniform vec2 u_padding;
uniform vec4 u_radii;
uniform vec4 u_tint;

// Per-fragment corner radius lookup — see RECT_FRAG_SRC for the
// quadrant convention.
float corner_radius(vec2 c, vec4 r)
{
	float top    = (c.x < 0.0) ? r.x : r.y;
	float bottom = (c.x < 0.0) ? r.w : r.z;
	return (c.y > 0.0) ? top : bottom;
}

void main()
{
	const int RADIUS = 20;

	// Rounded-rect SDF clip. 2-px AA band — see RECT_FRAG_SRC.
	vec2 p = v_uv * (u_size + 2.0 * u_padding) - u_padding;
	vec2 c = p - u_size * 0.5;
	vec2 half_sz = u_size * 0.5;
	float r = corner_radius(c, u_radii);
	r = min(r, min(half_sz.x, half_sz.y));
	vec2 q = abs(c) - (half_sz - vec2(r));
	float d = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
	float coverage = 1.0 - smoothstep(-1.0, 1.0, d);
	if (coverage <= 0.0) { discard; }

	// Vertical Gaussian on the H-blurred source.
	vec2 uv = gl_FragCoord.xy / u_canvas_size;
	vec4 total = vec4(0.0);
	float total_w = 0.0;
	for (int i = -RADIUS; i <= RADIUS; i++)
	{
		float fi = float(i);
		float w = exp(-(fi * fi) / (2.0 * u_sigma * u_sigma));
		total += texture2D(u_source, uv + fi * u_texel) * w;
		total_w += w;
	}
	vec4 blurred = total / total_w;

	// Optional tint, "tint over blurred" in premul space.
	vec3 tint_premul = u_tint.rgb * u_tint.a;
	vec3 result_rgb = tint_premul + blurred.rgb * (1.0 - u_tint.a);

	gl_FragColor = vec4(result_rgb * coverage, coverage);
}
"#;

// ── Fast (low-quality) variants of the backdrop shaders ────────────────
//
// Same separable-Gaussian pipeline as the full-quality pair above, but
// with `RADIUS = 4` instead of `RADIUS = 20`. That cuts each pass from
// 41 taps to 9 — a ~4.5× reduction in fragment-shader work — and
// shrinks the snapshot region the renderer has to copy from the main
// FBO from `target ± 21` to `target ± 5` pixels. Used during motion
// via [`super::low_quality_paint`]; the static frame is painted with
// the full-quality pair.
//
// `u_sigma` is still a uniform so the CPU side can clamp it to a
// value compatible with the smaller kernel (typically ≤ 2.0), keeping
// the kernel a sensible Gaussian rather than a sharply truncated one.
// Visually this means a thinner blur band during motion, which fades
// back to the full blur on the static frame.
pub( super ) const BACKDROP_FAST_BLUR_H_FRAG_SRC: &str = r#"
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_source;
uniform vec2 u_texel;
uniform vec2 u_canvas_size;
uniform float u_sigma;
void main()
{
	const int RADIUS = 4;
	vec2 uv = gl_FragCoord.xy / u_canvas_size;
	vec4 total = vec4(0.0);
	float total_w = 0.0;
	for (int i = -RADIUS; i <= RADIUS; i++)
	{
		float fi = float(i);
		float w = exp(-(fi * fi) / (2.0 * u_sigma * u_sigma));
		total += texture2D(u_source, uv + fi * u_texel) * w;
		total_w += w;
	}
	gl_FragColor = total / total_w;
}
"#;

pub( super ) const BACKDROP_FAST_COMPOSITE_FRAG_SRC: &str = r#"
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_source;
uniform vec2 u_canvas_size;
uniform vec2 u_texel;
uniform float u_sigma;
uniform vec2 u_size;
uniform vec2 u_padding;
uniform vec4 u_radii;
uniform vec4 u_tint;

// Per-fragment corner radius lookup — see RECT_FRAG_SRC for the
// quadrant convention.
float corner_radius(vec2 c, vec4 r)
{
	float top    = (c.x < 0.0) ? r.x : r.y;
	float bottom = (c.x < 0.0) ? r.w : r.z;
	return (c.y > 0.0) ? top : bottom;
}

void main()
{
	const int RADIUS = 4;

	// Rounded-rect SDF clip — identical to the full-quality variant.
	vec2 p = v_uv * (u_size + 2.0 * u_padding) - u_padding;
	vec2 c = p - u_size * 0.5;
	vec2 half_sz = u_size * 0.5;
	float r = corner_radius(c, u_radii);
	r = min(r, min(half_sz.x, half_sz.y));
	vec2 q = abs(c) - (half_sz - vec2(r));
	float d = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
	float coverage = 1.0 - smoothstep(-1.0, 1.0, d);
	if (coverage <= 0.0) { discard; }

	vec2 uv = gl_FragCoord.xy / u_canvas_size;
	vec4 total = vec4(0.0);
	float total_w = 0.0;
	for (int i = -RADIUS; i <= RADIUS; i++)
	{
		float fi = float(i);
		float w = exp(-(fi * fi) / (2.0 * u_sigma * u_sigma));
		total += texture2D(u_source, uv + fi * u_texel) * w;
		total_w += w;
	}
	vec4 blurred = total / total_w;

	vec3 tint_premul = u_tint.rgb * u_tint.a;
	vec3 result_rgb = tint_premul + blurred.rgb * (1.0 - u_tint.a);

	gl_FragColor = vec4(result_rgb * coverage, coverage);
}
"#;

// Inset shadow with CSS `Overlay` blend. Uses the same SDF dance as
// `SHADOW_INSET_FRAG_SRC` to compute `intensity` (outer-silhouette clip
// + Gaussian falloff from the offset-shifted inner SDF), then samples
// the snapshotted FBO content under the fragment and applies the
// per-channel Overlay formula:
//
//   overlay(base, src) = base < 0.5 ? 2 * base * src
//                                   : 1 - 2 * (1 - base) * (1 - src)
//
// Output is premultiplied with `mask = u_color.a * intensity` — so the
// standard `(ONE, ONE_MINUS_SRC_ALPHA)` blend on top of the main FBO
// yields `result = overlay_rgb * mask + base * (1 - mask)`, i.e. the
// Overlay effect modulated by the shadow mask, composed onto the
// original backdrop. The snapshot texture holds premultiplied content
// matching the main FBO, so we unpremultiply before applying Overlay.
//
// `u_canvas_size` is the main FBO's pixel size. `gl_FragCoord` has its
// origin at bottom-left in GLES, which matches the FBO's native
// orientation, so `gl_FragCoord.xy / u_canvas_size` gives the texture
// coordinates of the pixel we're about to write to. No Y-flip needed.
pub( super ) const SHADOW_INSET_OVERLAY_FRAG_SRC: &str = r##"
precision mediump float;
varying vec2 v_uv;
uniform vec2  u_size;
uniform vec2  u_padding;
uniform vec4  u_radii;
uniform float u_spread;
uniform float u_sigma;
uniform vec2  u_offset;
uniform vec4  u_color;
uniform sampler2D u_snapshot;
uniform vec2  u_canvas_size;

// Per-fragment corner radius lookup — see RECT_FRAG_SRC for the
// quadrant convention.
float corner_radius(vec2 c, vec4 r)
{
	float top    = (c.x < 0.0) ? r.x : r.y;
	float bottom = (c.x < 0.0) ? r.w : r.z;
	return (c.y > 0.0) ? top : bottom;
}

void main()
{
	vec2 p = v_uv * (u_size + 2.0 * u_padding) - u_padding;
	vec2 c = p - u_size * 0.5;

	// Outer silhouette clip — same as SHADOW_INSET_FRAG_SRC, 2-px AA band.
	vec2 half_outer = u_size * 0.5;
	float r_outer = corner_radius(c, u_radii);
	r_outer = min(r_outer, min(half_outer.x, half_outer.y));
	vec2 q_outer = abs(c) - (half_outer - vec2(r_outer));
	float d_outer = min(max(q_outer.x, q_outer.y), 0.0) + length(max(q_outer, 0.0)) - r_outer;
	float outer_coverage = 1.0 - smoothstep(-1.0, 1.0, d_outer);

	// Offset-shifted inner SDF → Gaussian intensity. Per-corner inner
	// radii match the outer shape eroded by `u_spread`.
	vec2 p_shifted = p - u_offset;
	vec2 c_shifted = p_shifted - u_size * 0.5;
	vec2 half_inner = half_outer - vec2(u_spread);
	half_inner = max(half_inner, vec2(1e-3));
	vec4 inner_radii = max(u_radii - vec4(u_spread), vec4(0.0));
	float r_inner = corner_radius(c_shifted, inner_radii);
	r_inner = min(r_inner, min(half_inner.x, half_inner.y));
	vec2 q_inner = abs(c_shifted) - (half_inner - vec2(r_inner));
	float d_inner = min(max(q_inner.x, q_inner.y), 0.0) + length(max(q_inner, 0.0)) - r_inner;

	float intensity;
	if (d_inner >= 0.0)
	{
		intensity = 1.0;
	}
	else
	{
		float s = max(u_sigma, 0.5);
		intensity = exp(-(d_inner * d_inner) / (2.0 * s * s));
	}
	intensity *= outer_coverage;

	float mask = u_color.a * intensity;

	// Sample the snapshot at the fragment's FBO position. The snapshot
	// holds premultiplied content; divide by alpha before applying the
	// straight-alpha Overlay formula. Guard alpha=0 to avoid NaNs.
	vec2 snap_uv = gl_FragCoord.xy / u_canvas_size;
	vec4 snap = texture2D(u_snapshot, snap_uv);
	vec3 base = snap.a > 0.0 ? snap.rgb / snap.a : vec3(0.0);
	vec3 src  = u_color.rgb;

	// Per-channel Overlay. `step(0.5, base)` yields 0 where base < 0.5
	// and 1 otherwise; `mix` picks multiply vs screen accordingly.
	vec3 multiply = 2.0 * base * src;
	vec3 screen   = 1.0 - 2.0 * (1.0 - base) * (1.0 - src);
	vec3 overlay  = mix(multiply, screen, step(0.5, base));

	// Output premul: `(overlay * mask, mask)`. Combined with the premul
	// over blend this replaces the base by `overlay` wherever mask == 1
	// and leaves it untouched where mask == 0.
	gl_FragColor = vec4(overlay * mask, mask);
}
"##;

