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

//! Construction + accessors for [`GlesCanvas`].
//!
//! The bulk of `GlesCanvas::new` is one big shader-compile +
//! uniform-lookup block: each of the shader programs gets compiled
//! from [`super::shaders`], its uniform locations are pulled out via
//! `get_uniform_location`, and both end up as `Copy` handles on the
//! struct so sub-canvases can share them for free. `sub_canvas` is
//! the same struct-literal again with the `new`-only bootstrap
//! elided (programs, VAO, default font all come from the parent).

use std::collections::HashMap;
use std::sync::Arc;

use fontdue::{ Font, LineMetrics, Metrics };
use glow::HasContext;

use crate::theme::{ FontRegistry, FontStyle };

use super::helpers::{ alloc_fbo_tex, bytemuck_cast_slice, compile_program, compile_program_with_attribs };
use super::shaders::
{
	BACKDROP_BLUR_H_FRAG_SRC, BACKDROP_COMPOSITE_FRAG_SRC,
	BACKDROP_FAST_BLUR_H_FRAG_SRC, BACKDROP_FAST_COMPOSITE_FRAG_SRC,
	BLIT_FRAG_SRC, BLIT_VERT_SRC,
	GLYPH_FRAG_SRC, GLYPH_BATCH_VERT_SRC, GLYPH_BATCH_FRAG_SRC,
	LINEAR_GRADIENT_FRAG_SRC, RADIAL_GRADIENT_FRAG_SRC,
	RECT_FRAG_SRC,
	CLIP_COMPOSITE_FRAG_SRC,
	SHADOW_INSET_FRAG_SRC, SHADOW_INSET_OVERLAY_FRAG_SRC, SHADOW_OUTER_FRAG_SRC,
	SUB_BLIT_FRAG_SRC,
	TEX_FRAG_SRC, VERT_SRC,
};
use super::{ GlesCanvas, GlesVersion };

impl GlesCanvas
{
	/// Build a GPU canvas of `width × height` physical px on an already-current
	/// EGL/GLES context. Compiles every shader program, looks up its uniforms,
	/// uploads the shared quad geometry and the glyph atlas (format chosen per ES
	/// profile), then allocates the persistent shadow FBO and makes it the active
	/// draw target — every draw writes into the FBO, and [`Self::present`] is the
	/// only call that blits it to the default framebuffer. `dpi_scale` and
	/// `global_alpha` start at 1.0; the default font comes from the process-wide
	/// cached handle. Panics if the FBO is incomplete or a program fails to link.
	pub fn new( gl: Arc<glow::Context>, version: GlesVersion, width: u32, height: u32 ) -> Self
	{
		let font_handle = crate::system_fonts::default_handle();
		let font        = font_handle.font.clone();
		let font_bytes  = font_handle.bytes.clone();
		let font_face   = font_handle.face;

		let rect_program            = compile_program( &gl, VERT_SRC,      RECT_FRAG_SRC            );
		let tex_program             = compile_program( &gl, VERT_SRC,      TEX_FRAG_SRC             );
		let glyph_program           = compile_program( &gl, VERT_SRC,      GLYPH_FRAG_SRC           );
		let glyph_batch_program     = compile_program_with_attribs( &gl, GLYPH_BATCH_VERT_SRC, GLYPH_BATCH_FRAG_SRC, &[ ( 0, "a_pos" ), ( 1, "a_uv" ) ] );
		let blit_program            = compile_program( &gl, BLIT_VERT_SRC, BLIT_FRAG_SRC            );
		let sub_blit_program        = compile_program( &gl, VERT_SRC,      SUB_BLIT_FRAG_SRC        );
		let linear_gradient_program = compile_program( &gl, VERT_SRC,      LINEAR_GRADIENT_FRAG_SRC );
		let radial_gradient_program = compile_program( &gl, VERT_SRC,      RADIAL_GRADIENT_FRAG_SRC );
		let shadow_outer_program    = compile_program( &gl, VERT_SRC,      SHADOW_OUTER_FRAG_SRC    );
		let shadow_inset_program    = compile_program( &gl, VERT_SRC,      SHADOW_INSET_FRAG_SRC    );
		let shadow_inset_overlay_program = compile_program( &gl, VERT_SRC, SHADOW_INSET_OVERLAY_FRAG_SRC );
		let backdrop_blur_h_program    = compile_program( &gl, BLIT_VERT_SRC, BACKDROP_BLUR_H_FRAG_SRC    );
		let backdrop_composite_program = compile_program( &gl, VERT_SRC,      BACKDROP_COMPOSITE_FRAG_SRC );
		let backdrop_fast_blur_h_program    = compile_program( &gl, BLIT_VERT_SRC, BACKDROP_FAST_BLUR_H_FRAG_SRC    );
		let backdrop_fast_composite_program = compile_program( &gl, VERT_SRC,      BACKDROP_FAST_COMPOSITE_FRAG_SRC );

		// SAFETY: every program in scope has just been linked successfully by
		// `compile_program` (which panics on link failure), so `get_uniform_location`
		// on these programs is well-defined. Each name argument is a `'static`
		// ASCII literal — `glow` will not invoke UB on a malformed C string.
		// `get_uniform_location` does not mutate the GL state machine, so this
		// block has no interaction with whatever bindings precede it.
		let (
			u_rect_mvp, u_rect_color, u_rect_size, u_rect_radii, u_rect_stroke, u_rect_pad,
			u_tex_mvp, u_tex_opacity, u_tex_sampler,
			u_glyph_mvp, u_glyph_color, u_glyph_opacity, u_glyph_sampler,
			u_glyph_uv_offset, u_glyph_uv_scale,
			u_blit_sampler,
			u_subblit_mvp, u_subblit_sampler, u_subblit_opacity, u_subblit_fade_bottom, u_subblit_height_px,
			u_lingrad_mvp, u_lingrad_lut, u_lingrad_dir, u_lingrad_size, u_lingrad_line_length,
			u_lingrad_radii, u_lingrad_pad, u_lingrad_lut_domain_min, u_lingrad_lut_domain_span,
			u_radgrad_mvp, u_radgrad_lut, u_radgrad_center, u_radgrad_radius_frac, u_radgrad_size,
			u_radgrad_radii, u_radgrad_pad, u_radgrad_lut_domain_min, u_radgrad_lut_domain_span,
			u_shadow_mvp, u_shadow_size, u_shadow_padding, u_shadow_radii, u_shadow_spread, u_shadow_sigma, u_shadow_color,
			u_inset_mvp, u_inset_size, u_inset_padding, u_inset_radii, u_inset_spread, u_inset_sigma, u_inset_offset, u_inset_color,
			u_inset_ov_mvp, u_inset_ov_size, u_inset_ov_padding, u_inset_ov_radii,
			u_inset_ov_spread, u_inset_ov_sigma, u_inset_ov_offset, u_inset_ov_color,
			u_inset_ov_snapshot, u_inset_ov_canvas_size,
			u_bd_h_source, u_bd_h_texel, u_bd_h_canvas_size, u_bd_h_sigma,
			u_bd_c_mvp, u_bd_c_source, u_bd_c_canvas_size, u_bd_c_texel, u_bd_c_sigma,
			u_bd_c_size, u_bd_c_padding, u_bd_c_radii, u_bd_c_tint,
			u_bd_fh_source, u_bd_fh_texel, u_bd_fh_canvas_size, u_bd_fh_sigma,
			u_bd_fc_mvp, u_bd_fc_source, u_bd_fc_canvas_size, u_bd_fc_texel, u_bd_fc_sigma,
			u_bd_fc_size, u_bd_fc_padding, u_bd_fc_radii, u_bd_fc_tint,
		) = unsafe
		{(
			gl.get_uniform_location( rect_program,     "u_mvp"     ).unwrap(),
			gl.get_uniform_location( rect_program,     "u_color"   ).unwrap(),
			gl.get_uniform_location( rect_program,     "u_size"    ).unwrap(),
			gl.get_uniform_location( rect_program,     "u_radii"   ).unwrap(),
			gl.get_uniform_location( rect_program,     "u_stroke"  ).unwrap(),
			gl.get_uniform_location( rect_program,     "u_pad"     ).unwrap(),
			gl.get_uniform_location( tex_program,      "u_mvp"     ).unwrap(),
			gl.get_uniform_location( tex_program,      "u_opacity" ).unwrap(),
			gl.get_uniform_location( tex_program,      "u_sampler" ).unwrap(),
			gl.get_uniform_location( glyph_program,    "u_mvp"     ).unwrap(),
			gl.get_uniform_location( glyph_program,    "u_color"   ).unwrap(),
			gl.get_uniform_location( glyph_program,    "u_opacity" ).unwrap(),
			gl.get_uniform_location( glyph_program,    "u_sampler"   ).unwrap(),
			gl.get_uniform_location( glyph_program,    "u_uv_offset" ).unwrap(),
			gl.get_uniform_location( glyph_program,    "u_uv_scale"  ).unwrap(),
			gl.get_uniform_location( blit_program,     "u_sampler" ).unwrap(),
			gl.get_uniform_location( sub_blit_program, "u_mvp"             ).unwrap(),
			gl.get_uniform_location( sub_blit_program, "u_sampler"         ).unwrap(),
			gl.get_uniform_location( sub_blit_program, "u_opacity"         ).unwrap(),
			gl.get_uniform_location( sub_blit_program, "u_fade_bottom_px"  ).unwrap(),
			gl.get_uniform_location( sub_blit_program, "u_height_px"       ).unwrap(),
			gl.get_uniform_location( linear_gradient_program, "u_mvp"             ).unwrap(),
			gl.get_uniform_location( linear_gradient_program, "u_lut"             ).unwrap(),
			gl.get_uniform_location( linear_gradient_program, "u_dir"             ).unwrap(),
			gl.get_uniform_location( linear_gradient_program, "u_size"            ).unwrap(),
			gl.get_uniform_location( linear_gradient_program, "u_line_length"     ).unwrap(),
			gl.get_uniform_location( linear_gradient_program, "u_radii"           ).unwrap(),
			gl.get_uniform_location( linear_gradient_program, "u_pad"             ).unwrap(),
			gl.get_uniform_location( linear_gradient_program, "u_lut_domain_min"  ).unwrap(),
			gl.get_uniform_location( linear_gradient_program, "u_lut_domain_span" ).unwrap(),
			gl.get_uniform_location( radial_gradient_program, "u_mvp"             ).unwrap(),
			gl.get_uniform_location( radial_gradient_program, "u_lut"             ).unwrap(),
			gl.get_uniform_location( radial_gradient_program, "u_center"          ).unwrap(),
			gl.get_uniform_location( radial_gradient_program, "u_radius_frac"     ).unwrap(),
			gl.get_uniform_location( radial_gradient_program, "u_size"            ).unwrap(),
			gl.get_uniform_location( radial_gradient_program, "u_radii"           ).unwrap(),
			gl.get_uniform_location( radial_gradient_program, "u_pad"             ).unwrap(),
			gl.get_uniform_location( radial_gradient_program, "u_lut_domain_min"  ).unwrap(),
			gl.get_uniform_location( radial_gradient_program, "u_lut_domain_span" ).unwrap(),
			gl.get_uniform_location( shadow_outer_program, "u_mvp"     ).unwrap(),
			gl.get_uniform_location( shadow_outer_program, "u_size"    ).unwrap(),
			gl.get_uniform_location( shadow_outer_program, "u_padding" ).unwrap(),
			gl.get_uniform_location( shadow_outer_program, "u_radii"   ).unwrap(),
			gl.get_uniform_location( shadow_outer_program, "u_spread"  ).unwrap(),
			gl.get_uniform_location( shadow_outer_program, "u_sigma"   ).unwrap(),
			gl.get_uniform_location( shadow_outer_program, "u_color"   ).unwrap(),
			gl.get_uniform_location( shadow_inset_program, "u_mvp"     ).unwrap(),
			gl.get_uniform_location( shadow_inset_program, "u_size"    ).unwrap(),
			gl.get_uniform_location( shadow_inset_program, "u_padding" ).unwrap(),
			gl.get_uniform_location( shadow_inset_program, "u_radii"   ).unwrap(),
			gl.get_uniform_location( shadow_inset_program, "u_spread"  ).unwrap(),
			gl.get_uniform_location( shadow_inset_program, "u_sigma"   ).unwrap(),
			gl.get_uniform_location( shadow_inset_program, "u_offset"  ).unwrap(),
			gl.get_uniform_location( shadow_inset_program, "u_color"   ).unwrap(),
			gl.get_uniform_location( shadow_inset_overlay_program, "u_mvp"         ).unwrap(),
			gl.get_uniform_location( shadow_inset_overlay_program, "u_size"        ).unwrap(),
			gl.get_uniform_location( shadow_inset_overlay_program, "u_padding"     ).unwrap(),
			gl.get_uniform_location( shadow_inset_overlay_program, "u_radii"       ).unwrap(),
			gl.get_uniform_location( shadow_inset_overlay_program, "u_spread"      ).unwrap(),
			gl.get_uniform_location( shadow_inset_overlay_program, "u_sigma"       ).unwrap(),
			gl.get_uniform_location( shadow_inset_overlay_program, "u_offset"      ).unwrap(),
			gl.get_uniform_location( shadow_inset_overlay_program, "u_color"       ).unwrap(),
			gl.get_uniform_location( shadow_inset_overlay_program, "u_snapshot"    ).unwrap(),
			gl.get_uniform_location( shadow_inset_overlay_program, "u_canvas_size" ).unwrap(),
			gl.get_uniform_location( backdrop_blur_h_program, "u_source"      ).unwrap(),
			gl.get_uniform_location( backdrop_blur_h_program, "u_texel"       ).unwrap(),
			gl.get_uniform_location( backdrop_blur_h_program, "u_canvas_size" ).unwrap(),
			gl.get_uniform_location( backdrop_blur_h_program, "u_sigma"       ).unwrap(),
			gl.get_uniform_location( backdrop_composite_program, "u_mvp"         ).unwrap(),
			gl.get_uniform_location( backdrop_composite_program, "u_source"      ).unwrap(),
			gl.get_uniform_location( backdrop_composite_program, "u_canvas_size" ).unwrap(),
			gl.get_uniform_location( backdrop_composite_program, "u_texel"       ).unwrap(),
			gl.get_uniform_location( backdrop_composite_program, "u_sigma"       ).unwrap(),
			gl.get_uniform_location( backdrop_composite_program, "u_size"        ).unwrap(),
			gl.get_uniform_location( backdrop_composite_program, "u_padding"     ).unwrap(),
			gl.get_uniform_location( backdrop_composite_program, "u_radii"       ).unwrap(),
			gl.get_uniform_location( backdrop_composite_program, "u_tint"        ).unwrap(),
			gl.get_uniform_location( backdrop_fast_blur_h_program, "u_source"      ).unwrap(),
			gl.get_uniform_location( backdrop_fast_blur_h_program, "u_texel"       ).unwrap(),
			gl.get_uniform_location( backdrop_fast_blur_h_program, "u_canvas_size" ).unwrap(),
			gl.get_uniform_location( backdrop_fast_blur_h_program, "u_sigma"       ).unwrap(),
			gl.get_uniform_location( backdrop_fast_composite_program, "u_mvp"         ).unwrap(),
			gl.get_uniform_location( backdrop_fast_composite_program, "u_source"      ).unwrap(),
			gl.get_uniform_location( backdrop_fast_composite_program, "u_canvas_size" ).unwrap(),
			gl.get_uniform_location( backdrop_fast_composite_program, "u_texel"       ).unwrap(),
			gl.get_uniform_location( backdrop_fast_composite_program, "u_sigma"       ).unwrap(),
			gl.get_uniform_location( backdrop_fast_composite_program, "u_size"        ).unwrap(),
			gl.get_uniform_location( backdrop_fast_composite_program, "u_padding"     ).unwrap(),
			gl.get_uniform_location( backdrop_fast_composite_program, "u_radii"       ).unwrap(),
			gl.get_uniform_location( backdrop_fast_composite_program, "u_tint"        ).unwrap(),
		)};

		let ( u_glyph_batch_color, u_glyph_batch_opacity, u_glyph_batch_sampler ) = unsafe
		{(
			gl.get_uniform_location( glyph_batch_program, "u_color"   ).unwrap(),
			gl.get_uniform_location( glyph_batch_program, "u_opacity" ).unwrap(),
			gl.get_uniform_location( glyph_batch_program, "u_sampler" ).unwrap(),
		)};

		let clip_composite_program = compile_program( &gl, VERT_SRC, CLIP_COMPOSITE_FRAG_SRC );
		let ( u_clip_mvp, u_clip_layer, u_clip_mask, u_clip_canvas, u_clip_bbox ) = unsafe
		{(
			gl.get_uniform_location( clip_composite_program, "u_mvp"    ).unwrap(),
			gl.get_uniform_location( clip_composite_program, "u_layer"  ).unwrap(),
			gl.get_uniform_location( clip_composite_program, "u_mask"   ).unwrap(),
			gl.get_uniform_location( clip_composite_program, "u_canvas" ).unwrap(),
			gl.get_uniform_location( clip_composite_program, "u_bbox"   ).unwrap(),
		)};

		let ( glyph_batch_vao, glyph_batch_vbo ) = unsafe
		{
			let vao = gl.create_vertex_array().unwrap();
			let vbo = gl.create_buffer().unwrap();
			gl.bind_vertex_array( Some( vao ) );
			gl.bind_buffer( glow::ARRAY_BUFFER, Some( vbo ) );
			gl.enable_vertex_attrib_array( 0 );
			gl.vertex_attrib_pointer_f32( 0, 2, glow::FLOAT, false, 16, 0 );
			gl.enable_vertex_attrib_array( 1 );
			gl.vertex_attrib_pointer_f32( 1, 2, glow::FLOAT, false, 16, 8 );
			gl.bind_vertex_array( None );
			( vao, vbo )
		};

		let quad_vertices: [f32; 12] = [
			0.0, 0.0,  1.0, 0.0,  0.0, 1.0,
			1.0, 0.0,  1.0, 1.0,  0.0, 1.0,
		];
		// SAFETY: the GL context is current (caller contract). We allocate a
		// VAO + VBO, upload `quad_vertices` (24 bytes, statically known size,
		// matches `STATIC_DRAW` semantics), and configure attribute 0 to read
		// 2-float vertices from the VBO at offset 0 with stride 8. The vertex
		// attrib pointer is bound to `vbo` because `vbo` is the current
		// `ARRAY_BUFFER` binding when `vertex_attrib_pointer_f32` runs. We
		// unbind the VAO at the end so we don't strand a binding the rest of
		// `new` might inherit; the VBO remains attached to the VAO and is not
		// touched again until `Drop`.
		let ( quad_vao, quad_vbo ) = unsafe
		{
			let vao = gl.create_vertex_array().unwrap();
			let vbo = gl.create_buffer().unwrap();
			gl.bind_vertex_array( Some( vao ) );
			gl.bind_buffer( glow::ARRAY_BUFFER, Some( vbo ) );
			gl.buffer_data_u8_slice(
				glow::ARRAY_BUFFER,
				bytemuck_cast_slice( &quad_vertices ),
				glow::STATIC_DRAW,
			);
			gl.enable_vertex_attrib_array( 0 );
			gl.vertex_attrib_pointer_f32( 0, 2, glow::FLOAT, false, 8, 0 );
			gl.bind_vertex_array( None );
			( vao, vbo )
		};

		// Build the persistent FBO (shadow canvas) and bind it as the active
		// draw target. From here on, every draw method writes into the FBO;
		// `present` is the only call that switches to the default framebuffer.
		//
		// SAFETY: the GL context is current. `create_framebuffer` allocates a
		// fresh FBO name. `alloc_fbo_tex` (an `unsafe fn`) requires the same
		// invariant and returns a colour-renderable RGBA texture sized to the
		// caller's width × height — the call is sound because both invariants
		// are established. The bind + framebuffer_texture_2d pair attach the
		// texture to COLOR_ATTACHMENT0; `check_framebuffer_status` is the
		// canonical post-condition check and will assert before we return a
		// handle to a broken FBO. The trailing GL state (`BLEND`, `blend_func`,
		// `viewport`, `clear`) is the canvas-wide default state every draw
		// method assumes — see the comment block beside `blend_func` for why
		// `(ONE, ONE_MINUS_SRC_ALPHA)` is the correct pair for premultiplied
		// shaders.
		let ( fbo, fbo_tex ) = unsafe
		{
			let fbo     = gl.create_framebuffer().expect( "create_framebuffer" );
			let fbo_tex = alloc_fbo_tex( &gl, version, width, height );
			gl.bind_framebuffer( glow::FRAMEBUFFER, Some( fbo ) );
			gl.framebuffer_texture_2d(
				glow::FRAMEBUFFER, glow::COLOR_ATTACHMENT0,
				glow::TEXTURE_2D, Some( fbo_tex ), 0,
			);
			let status = gl.check_framebuffer_status( glow::FRAMEBUFFER );
			assert_eq!( status, glow::FRAMEBUFFER_COMPLETE, "FBO incomplete: 0x{status:x}" );

			gl.enable( glow::BLEND );
			// Premultiplied-alpha "over" composite: `result = src + dst * (1 - src.a)`
			// applied uniformly to both colour and alpha. All shaders in this
			// pipeline emit premul colour (see the note above `RECT_FRAG_SRC`),
			// so `(ONE, ONE_MINUS_SRC_ALPHA)` is correct for both channels.
			// Premultiplied inputs are also a requirement for the plus-lighter
			// and overlay blend modes.
			gl.blend_func( glow::ONE, glow::ONE_MINUS_SRC_ALPHA );
			gl.viewport( 0, 0, width as i32, height as i32 );
			// Clear the FBO once at startup so the first frame is not garbage.
			gl.clear_color( 0.0, 0.0, 0.0, 0.0 );
			gl.clear( glow::COLOR_BUFFER_BIT );
			( fbo, fbo_tex )
		};

		// Atlas format picked per ES profile: GL_R8 / GL_RED on ES3
		// (GL_LUMINANCE is deprecated in ES3 core and some mobile drivers
		// — Mali, certain Adreno — return GL_INVALID_ENUM for it), legacy
		// GL_LUMINANCE on ES2 where the modern single-channel formats do
		// not exist. The glyph fragment shader samples `.r` and works for
		// both: GL_RED puts the byte in .r directly, GL_LUMINANCE
		// replicates it into .r=.g=.b, so reading `.r` gives the same
		// coverage value either way.
		let ( atlas_internal, atlas_format ) = match version
		{
			GlesVersion::V3 => ( glow::R8 as i32,        glow::RED       ),
			GlesVersion::V2 => ( glow::LUMINANCE as i32, glow::LUMINANCE ),
		};
		let atlas_texture = unsafe
		{
			let tex = gl.create_texture().unwrap();
			gl.bind_texture( glow::TEXTURE_2D, Some( tex ) );
			gl.pixel_store_i32( glow::UNPACK_ALIGNMENT, 1 );
			gl.tex_image_2d(
				glow::TEXTURE_2D, 0, atlas_internal,
				super::ATLAS_SIZE as i32, super::ATLAS_SIZE as i32, 0,
				atlas_format, glow::UNSIGNED_BYTE,
				glow::PixelUnpackData::Slice( None ),
			);
			gl.pixel_store_i32( glow::UNPACK_ALIGNMENT, 4 );
			gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32 );
			gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32 );
			gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32 );
			gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32 );
			gl.bind_texture( glow::TEXTURE_2D, None );
			tex
		};

		Self
		{
			gl,
			version,
			font,
			font_bytes,
			font_face,
			font_registry:  None,
			dpi_scale:      1.0,
			layout_viewport: None,
			density:        None,
			global_alpha:   1.0,
			width,
			height,
			rect_program,
			tex_program,
			glyph_program,
			blit_program,
			sub_blit_program,
			linear_gradient_program,
			radial_gradient_program,
			shadow_outer_program,
			shadow_inset_program,
			shadow_inset_overlay_program,
			quad_vao,
			_quad_vbo: quad_vbo,
			u_rect_mvp,
			u_rect_color,
			u_rect_size,
			u_rect_radii,
			u_rect_stroke,
			u_rect_pad,
			u_tex_mvp,
			u_tex_opacity,
			u_tex_sampler,
			u_glyph_mvp,
			u_glyph_color,
			u_glyph_opacity,
			u_glyph_sampler,
			u_glyph_uv_offset,
			u_glyph_uv_scale,
			glyph_batch_program,
			u_glyph_batch_color,
			u_glyph_batch_opacity,
			u_glyph_batch_sampler,
			glyph_batch_vao,
			glyph_batch_vbo,
			u_blit_sampler,
			u_subblit_mvp,
			u_subblit_sampler,
			u_subblit_opacity,
			u_subblit_fade_bottom,
			u_subblit_height_px,
			u_lingrad_mvp,
			u_lingrad_lut,
			u_lingrad_dir,
			u_lingrad_size,
			u_lingrad_line_length,
			u_lingrad_radii,
			u_lingrad_pad,
			u_lingrad_lut_domain_min,
			u_lingrad_lut_domain_span,
			u_radgrad_mvp,
			u_radgrad_lut,
			u_radgrad_center,
			u_radgrad_radius_frac,
			u_radgrad_size,
			u_radgrad_radii,
			u_radgrad_pad,
			u_radgrad_lut_domain_min,
			u_radgrad_lut_domain_span,
			u_shadow_mvp,
			u_shadow_size,
			u_shadow_padding,
			u_shadow_radii,
			u_shadow_spread,
			u_shadow_sigma,
			u_shadow_color,
			u_inset_mvp,
			u_inset_size,
			u_inset_padding,
			u_inset_radii,
			u_inset_spread,
			u_inset_sigma,
			u_inset_offset,
			u_inset_color,
			u_inset_ov_mvp,
			u_inset_ov_size,
			u_inset_ov_padding,
			u_inset_ov_radii,
			u_inset_ov_spread,
			u_inset_ov_sigma,
			u_inset_ov_offset,
			u_inset_ov_color,
			u_inset_ov_snapshot,
			u_inset_ov_canvas_size,
			backdrop_blur_h_program,
			u_bd_h_source,
			u_bd_h_texel,
			u_bd_h_canvas_size,
			u_bd_h_sigma,
			backdrop_composite_program,
			u_bd_c_mvp,
			u_bd_c_source,
			u_bd_c_canvas_size,
			u_bd_c_texel,
			u_bd_c_sigma,
			u_bd_c_size,
			u_bd_c_padding,
			u_bd_c_radii,
			u_bd_c_tint,
			backdrop_fast_blur_h_program,
			u_bd_fh_source,
			u_bd_fh_texel,
			u_bd_fh_canvas_size,
			u_bd_fh_sigma,
			backdrop_fast_composite_program,
			u_bd_fc_mvp,
			u_bd_fc_source,
			u_bd_fc_canvas_size,
			u_bd_fc_texel,
			u_bd_fc_sigma,
			u_bd_fc_size,
			u_bd_fc_padding,
			u_bd_fc_radii,
			u_bd_fc_tint,
			atlas_texture,
			atlas_format,
			atlas_cursor_x:      0,
			atlas_cursor_y:      0,
			atlas_row_height:    0,
			glyph_cache:         HashMap::new(),
			image_cache:         HashMap::new(),
			image_cache_lru:     std::collections::VecDeque::new(),
			image_cache_bytes:   0,
			gradient_lut_cache:  HashMap::new(),
			clip_scissor:   None,
			clip_layer:        None,
			clip_layer_active: false,
			clip_mask_tex:     None,
			clip_bbox:         crate::types::Rect::default(),
			clip_composite_program,
			u_clip_mvp,
			u_clip_layer,
			u_clip_mask,
			u_clip_canvas,
			u_clip_bbox,
			fbo,
			fbo_tex,
			aux_a:          None,
			aux_b:          None,
		}
	}

	/// Build a sub-canvas: a separate render target sharing this canvas's GL
	/// context, font, shader programs, geometry, and uniform locations, but
	/// with its own FBO + color texture sized to `width × height`. Used to
	/// render content into an off-screen target that can then be composited
	/// back via [`Self::blit`].
	///
	/// The returned canvas inherits the parent's `dpi_scale` and `global_alpha`
	/// (so glyphs render at the same pixel size). Its glyph cache starts empty
	/// — re-rasterising on first use is the cost of not sharing GL textures
	/// across canvases.
	pub fn sub_canvas( &self, width: u32, height: u32 ) -> GlesCanvas
	{
		let gl = Arc::clone( &self.gl );
		// SAFETY: `self.gl` is the same context held by `self`; if `self` was
		// constructed soundly its context is current on this thread. We
		// allocate a new FBO + colour texture and attach them, then assert
		// completeness. We deliberately leave `gl` with the new FBO bound
		// instead of restoring the parent's binding — every draw method goes
		// through `activate_target`, which re-binds the canvas's own FBO
		// before issuing any draw call, so the transient binding cannot be
		// observed by other code on this thread.
		let ( fbo, fbo_tex ) = unsafe
		{
			let fbo     = gl.create_framebuffer().expect( "create_framebuffer" );
			let fbo_tex = alloc_fbo_tex( &gl, self.version, width, height );
			gl.bind_framebuffer( glow::FRAMEBUFFER, Some( fbo ) );
			gl.framebuffer_texture_2d(
				glow::FRAMEBUFFER, glow::COLOR_ATTACHMENT0,
				glow::TEXTURE_2D, Some( fbo_tex ), 0,
			);
			let status = gl.check_framebuffer_status( glow::FRAMEBUFFER );
			assert_eq!( status, glow::FRAMEBUFFER_COMPLETE, "sub-FBO incomplete: 0x{status:x}" );
			( fbo, fbo_tex )
		};

		let atlas_format = self.atlas_format;
		let atlas_internal = match self.version
		{
			GlesVersion::V3 => glow::R8 as i32,
			GlesVersion::V2 => glow::LUMINANCE as i32,
		};
		let atlas_texture = unsafe
		{
			let tex = gl.create_texture().unwrap();
			gl.bind_texture( glow::TEXTURE_2D, Some( tex ) );
			gl.pixel_store_i32( glow::UNPACK_ALIGNMENT, 1 );
			gl.tex_image_2d(
				glow::TEXTURE_2D, 0, atlas_internal,
				super::ATLAS_SIZE as i32, super::ATLAS_SIZE as i32, 0,
				atlas_format, glow::UNSIGNED_BYTE,
				glow::PixelUnpackData::Slice( None ),
			);
			gl.pixel_store_i32( glow::UNPACK_ALIGNMENT, 4 );
			gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32 );
			gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32 );
			gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32 );
			gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32 );
			gl.bind_texture( glow::TEXTURE_2D, None );
			tex
		};

		GlesCanvas
		{
			gl,
			version:           self.version,
			font:              Arc::clone( &self.font ),
			font_bytes:        Arc::clone( &self.font_bytes ),
			font_face:         self.font_face,
			font_registry:     self.font_registry.as_ref().map( Arc::clone ),
			dpi_scale:         self.dpi_scale,
			layout_viewport:   Some( self.layout_viewport.unwrap_or(
				( self.width as f32, self.height as f32 ) ) ),
			density:           self.density,
			global_alpha:      self.global_alpha,
			width,
			height,
			rect_program:              self.rect_program,
			tex_program:               self.tex_program,
			glyph_program:             self.glyph_program,
			blit_program:              self.blit_program,
			sub_blit_program:          self.sub_blit_program,
			linear_gradient_program:   self.linear_gradient_program,
			radial_gradient_program:   self.radial_gradient_program,
			shadow_outer_program:      self.shadow_outer_program,
			shadow_inset_program:      self.shadow_inset_program,
			shadow_inset_overlay_program:      self.shadow_inset_overlay_program,
			backdrop_blur_h_program:           self.backdrop_blur_h_program,
			backdrop_composite_program:        self.backdrop_composite_program,
			backdrop_fast_blur_h_program:      self.backdrop_fast_blur_h_program,
			backdrop_fast_composite_program:   self.backdrop_fast_composite_program,
			quad_vao:          self.quad_vao,
			_quad_vbo:         self._quad_vbo,
			u_rect_mvp:        self.u_rect_mvp,
			u_rect_color:      self.u_rect_color,
			u_rect_size:       self.u_rect_size,
			u_rect_radii:      self.u_rect_radii,
			u_rect_stroke:     self.u_rect_stroke,
			u_rect_pad:        self.u_rect_pad,
			u_tex_mvp:         self.u_tex_mvp,
			u_tex_opacity:     self.u_tex_opacity,
			u_tex_sampler:     self.u_tex_sampler,
			u_glyph_mvp:         self.u_glyph_mvp,
			u_glyph_color:       self.u_glyph_color,
			u_glyph_opacity:     self.u_glyph_opacity,
			u_glyph_sampler:     self.u_glyph_sampler,
			u_glyph_uv_offset:   self.u_glyph_uv_offset,
			u_glyph_uv_scale:    self.u_glyph_uv_scale,
			glyph_batch_program:   self.glyph_batch_program,
			u_glyph_batch_color:   self.u_glyph_batch_color,
			u_glyph_batch_opacity: self.u_glyph_batch_opacity,
			u_glyph_batch_sampler: self.u_glyph_batch_sampler,
			glyph_batch_vao:       self.glyph_batch_vao,
			glyph_batch_vbo:       self.glyph_batch_vbo,
			u_blit_sampler:    self.u_blit_sampler,
			u_subblit_mvp:         self.u_subblit_mvp,
			u_subblit_sampler:     self.u_subblit_sampler,
			u_subblit_opacity:     self.u_subblit_opacity,
			u_subblit_fade_bottom: self.u_subblit_fade_bottom,
			u_subblit_height_px:   self.u_subblit_height_px,
			u_lingrad_mvp:             self.u_lingrad_mvp,
			u_lingrad_lut:             self.u_lingrad_lut,
			u_lingrad_dir:             self.u_lingrad_dir,
			u_lingrad_size:            self.u_lingrad_size,
			u_lingrad_line_length:     self.u_lingrad_line_length,
			u_lingrad_radii:           self.u_lingrad_radii,
			u_lingrad_pad:             self.u_lingrad_pad,
			u_lingrad_lut_domain_min:  self.u_lingrad_lut_domain_min,
			u_lingrad_lut_domain_span: self.u_lingrad_lut_domain_span,
			u_radgrad_mvp:             self.u_radgrad_mvp,
			u_radgrad_lut:             self.u_radgrad_lut,
			u_radgrad_center:          self.u_radgrad_center,
			u_radgrad_radius_frac:     self.u_radgrad_radius_frac,
			u_radgrad_size:            self.u_radgrad_size,
			u_radgrad_radii:           self.u_radgrad_radii,
			u_radgrad_pad:             self.u_radgrad_pad,
			u_radgrad_lut_domain_min:  self.u_radgrad_lut_domain_min,
			u_radgrad_lut_domain_span: self.u_radgrad_lut_domain_span,
			u_shadow_mvp:      self.u_shadow_mvp,
			u_shadow_size:     self.u_shadow_size,
			u_shadow_padding:  self.u_shadow_padding,
			u_shadow_radii:    self.u_shadow_radii,
			u_shadow_spread:   self.u_shadow_spread,
			u_shadow_sigma:    self.u_shadow_sigma,
			u_shadow_color:    self.u_shadow_color,
			u_inset_mvp:       self.u_inset_mvp,
			u_inset_size:      self.u_inset_size,
			u_inset_padding:   self.u_inset_padding,
			u_inset_radii:     self.u_inset_radii,
			u_inset_spread:    self.u_inset_spread,
			u_inset_sigma:     self.u_inset_sigma,
			u_inset_offset:    self.u_inset_offset,
			u_inset_color:     self.u_inset_color,
			u_inset_ov_mvp:         self.u_inset_ov_mvp,
			u_inset_ov_size:        self.u_inset_ov_size,
			u_inset_ov_padding:     self.u_inset_ov_padding,
			u_inset_ov_radii:       self.u_inset_ov_radii,
			u_inset_ov_spread:      self.u_inset_ov_spread,
			u_inset_ov_sigma:       self.u_inset_ov_sigma,
			u_inset_ov_offset:      self.u_inset_ov_offset,
			u_inset_ov_color:       self.u_inset_ov_color,
			u_inset_ov_snapshot:    self.u_inset_ov_snapshot,
			u_inset_ov_canvas_size: self.u_inset_ov_canvas_size,
			u_bd_h_source:      self.u_bd_h_source,
			u_bd_h_texel:       self.u_bd_h_texel,
			u_bd_h_canvas_size: self.u_bd_h_canvas_size,
			u_bd_h_sigma:       self.u_bd_h_sigma,
			u_bd_c_mvp:         self.u_bd_c_mvp,
			u_bd_c_source:      self.u_bd_c_source,
			u_bd_c_canvas_size: self.u_bd_c_canvas_size,
			u_bd_c_texel:       self.u_bd_c_texel,
			u_bd_c_sigma:       self.u_bd_c_sigma,
			u_bd_c_size:        self.u_bd_c_size,
			u_bd_c_padding:     self.u_bd_c_padding,
			u_bd_c_radii:       self.u_bd_c_radii,
			u_bd_c_tint:        self.u_bd_c_tint,
			u_bd_fh_source:      self.u_bd_fh_source,
			u_bd_fh_texel:       self.u_bd_fh_texel,
			u_bd_fh_canvas_size: self.u_bd_fh_canvas_size,
			u_bd_fh_sigma:       self.u_bd_fh_sigma,
			u_bd_fc_mvp:         self.u_bd_fc_mvp,
			u_bd_fc_source:      self.u_bd_fc_source,
			u_bd_fc_canvas_size: self.u_bd_fc_canvas_size,
			u_bd_fc_texel:       self.u_bd_fc_texel,
			u_bd_fc_sigma:       self.u_bd_fc_sigma,
			u_bd_fc_size:        self.u_bd_fc_size,
			u_bd_fc_padding:     self.u_bd_fc_padding,
			u_bd_fc_radii:       self.u_bd_fc_radii,
			u_bd_fc_tint:        self.u_bd_fc_tint,
			atlas_texture,
			atlas_format,
			atlas_cursor_x:     0,
			atlas_cursor_y:     0,
			atlas_row_height:   0,
			glyph_cache:        HashMap::new(),
			image_cache:        HashMap::new(),
			image_cache_lru:    std::collections::VecDeque::new(),
			image_cache_bytes:  0,
			gradient_lut_cache: HashMap::new(),
			clip_scissor:       None,
			clip_layer:             None,
			clip_layer_active:      false,
			clip_mask_tex:          None,
			clip_bbox:              crate::types::Rect::default(),
			clip_composite_program: self.clip_composite_program,
			u_clip_mvp:             self.u_clip_mvp,
			u_clip_layer:           self.u_clip_layer,
			u_clip_mask:            self.u_clip_mask,
			u_clip_canvas:          self.u_clip_canvas,
			u_clip_bbox:            self.u_clip_bbox,
			fbo,
			fbo_tex,
			aux_a:              None,
			aux_b:              None,
		}
	}

	/// `( width, height )` of the FBO in physical px.
	pub fn size( &self ) -> ( u32, u32 ) { ( self.width, self.height ) }

	/// Discard all cached gradient LUT textures. Call after a theme change
	/// so stale LUTs for old palette colours are freed and rebuilt fresh.
	pub fn clear_gradient_cache( &mut self )
	{
		// SAFETY: GL context is current (canvas invariant). Each texture in
		// `gradient_lut_cache` was created through this same context in
		// `gradient.rs::ensure_lut`, so deleting them through the same context
		// is well-defined. `drain` consumes the entries so the same name is
		// never deleted twice.
		unsafe
		{
			for ( _, tex ) in self.gradient_lut_cache.drain()
			{
				self.gl.delete_texture( tex );
			}
		}
	}

	/// DPI scale factor applied to font sizes before rasterisation.
	pub fn dpi_scale( &self ) -> f32 { self.dpi_scale }

	/// Set the DPI scale factor applied to font sizes.
	pub fn set_dpi_scale( &mut self, s: f32 ) { self.dpi_scale = s; }

	/// Global alpha multiplier applied to every draw (0.0 transparent, 1.0 opaque).
	pub fn global_alpha( &self ) -> f32 { self.global_alpha }

	/// Set the global alpha multiplier applied to every draw.
	pub fn set_global_alpha( &mut self, a: f32 ) { self.global_alpha = a; }

	/// The canvas default font, used when no specific face is resolved.
	pub fn font( &self ) -> &Font { &self.font }

	/// Install a theme font registry so [`Self::font_for`] can resolve
	/// family+weight+style triples declared by the theme's `fonts` block.
	pub fn set_font_registry( &mut self, registry: Arc<FontRegistry> )
	{
		self.font_registry = Some( registry );
	}

	/// Resolve a specific font from the theme registry, falling back to the
	/// canvas' default [`Self::font`] when no registry is installed or the
	/// triple cannot be satisfied.
	pub fn font_for( &self, family: &str, weight: u16, style: FontStyle ) -> Arc<Font>
	{
		self.font_registry
			.as_ref()
			.and_then( |r| r.resolve( family, weight, style ) )
			.unwrap_or_else( || Arc::clone( &self.font ) )
	}

	/// Pick the right font for `ch`. Tries the primary [`Self::font`]
	/// first; on a miss, delegates to the crate-private system-fonts
	/// fallback chain (lazy load of the relevant Noto pack). Falls
	/// back to the primary (which paints a `.notdef` box) when no
	/// installed fallback covers the codepoint.
	pub fn font_for_char( &self, ch: char ) -> Arc<Font>
	{
		if self.font.lookup_glyph_index( ch ) != 0
		{
			return Arc::clone( &self.font );
		}
		crate::system_fonts::lookup( ch ).unwrap_or_else( || Arc::clone( &self.font ) )
	}

	/// Glyph metrics for `ch` at `size` logical px, resolved through the fallback
	/// chain and pre-scaled by `dpi_scale`.
	pub fn font_metrics( &self, ch: char, size: f32 ) -> Metrics
	{
		self.font_for_char( ch ).metrics( ch, size * self.dpi_scale )
	}

	/// Horizontal line metrics of the default font at `size` logical px (`None`
	/// if the font lacks them), pre-scaled by `dpi_scale`.
	pub fn font_line_metrics( &self, size: f32 ) -> Option<LineMetrics>
	{
		self.font.horizontal_line_metrics( size * self.dpi_scale )
	}

	/// Resize the FBO and viewport. The previous color attachment is freed and
	/// a fresh one of the new size is attached — frame-N pixels are dropped, so
	/// the caller should expect to do a full redraw immediately after a resize.
	pub fn resize( &mut self, width: u32, height: u32 )
	{
		if width == self.width && height == self.height { return; }
		self.width  = width;
		self.height = height;
		// SAFETY: GL context is current (canvas invariant). Sequence:
		// release the old colour attachment (created via this context in
		// `new`/`sub_canvas`/last `resize`), allocate a fresh one of the
		// new size, swap it in for `COLOR_ATTACHMENT0` of the existing FBO,
		// resize the viewport to match, and clear so the first frame after
		// resize is well-defined RGBA. `self.fbo` is unchanged and remains
		// valid; only its colour attachment is replaced.
		unsafe
		{
			self.gl.delete_texture( self.fbo_tex );
			self.fbo_tex = alloc_fbo_tex( &self.gl, self.version, width, height );
			self.gl.bind_framebuffer( glow::FRAMEBUFFER, Some( self.fbo ) );
			self.gl.framebuffer_texture_2d(
				glow::FRAMEBUFFER, glow::COLOR_ATTACHMENT0,
				glow::TEXTURE_2D, Some( self.fbo_tex ), 0,
			);
			// The clip layer was sized for the old dimensions — free it so the next
			// path clip re-allocates at the new size; any in-flight clip is dropped.
			if let Some( ( fbo, tex ) ) = self.clip_layer.take()
			{
				self.gl.delete_framebuffer( fbo );
				self.gl.delete_texture( tex );
			}
			if let Some( tex ) = self.clip_mask_tex.take()
			{
				self.gl.delete_texture( tex );
			}
			self.gl.viewport( 0, 0, width as i32, height as i32 );
			self.gl.clear_color( 0.0, 0.0, 0.0, 0.0 );
			self.gl.clear( glow::COLOR_BUFFER_BIT );
		}
		self.clip_layer_active = false;
		// Auxiliary textures were sized for the old dimensions — drop them so
		// the next effect that needs them re-allocates at the new size.
		self.invalidate_aux();
	}
}
