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

//! GPU-accelerated rendering backend using EGL + GLES2 / GLES3.
//!
//! Mirrors the public surface of the software backend so that
//! [`crate::core::Canvas`] can route widget draw calls to either backend
//! by `match self`. The EGL context bootstrap lives in
//! [`crate::egl_context`] — this module is just the renderer that runs
//! once a context is current.
//!
//! Rect clipping is implemented with `glScissor`. When
//! [`GlesCanvas::set_clip_rects`] receives multiple rects the
//! bounding-box union is used as the scissor — coarse, but the
//! partial-redraw path normally clusters the dirty rects of 1–3
//! widgets so the union is barely larger than the sum. Arbitrary
//! path clipping ([`GlesCanvas::set_clip_path`]) captures the clipped
//! draws into an offscreen layer and composites it back through an
//! anti-aliased coverage mask, for a smooth clipped edge.
//!
//! # Submodule layout
//!
//! * `setup` — `GlesCanvas::{new, sub_canvas, resize, set_font_registry,
//!   font_for, dpi/alpha accessors}`.
//! * `framebuffer` — `GlesCanvas::{blit, present, activate_target,
//!   borrowed_texture, read_rgba_pixels, ensure_aux_*, snapshot_fbo_region,
//!   fill_backdrop, invalidate_aux}` — everything that manipulates the
//!   FBO or auxiliary snapshot textures.
//! * `clip` — `GlesCanvas::{set_clip_rects, clear_clip, set_scissor,
//!   scissor_pixels, fill, clear, clear_rects_transparent}`.
//! * `primitives` — `GlesCanvas::{fill_rect, fill_linear_gradient_rect,
//!   fill_radial_gradient_rect, fill_shadow_outer, fill_shadow_inset,
//!   stroke_rect, draw_line}`.
//! * `text` — `GlesCanvas::{draw_text, measure_text}`.
//! * `image` — `GlesCanvas::draw_image_data`.
//! * `shaders` — GLSL ES 1.00 shader sources (const strings).
//! * `helpers` — free functions: `ortho_rect`, `compile_program`,
//!   `alloc_fbo_tex`, `upload_*_texture`, handle extractors.
//!   System-font resolution lives in `crate::system_fonts`.
//! * `raii` — `FboBinding` / `ProgramBinding` scoped guards for the
//!   handful of operations that change global GL state for a scope
//!   and must guarantee restoration even on early return / panic
//!   (presently only `present`). The renderer otherwise relies on
//!   `activate_target`'s lazy re-bind at the entry of each draw
//!   method — see the module's own doc for when to use the guards
//!   and when not to.

use std::collections::{ HashMap, VecDeque };
use std::sync::Arc;

use fontdue::Font;
use glow::HasContext;

use crate::theme::FontRegistry;
use crate::types::Rect;

pub( crate ) mod shaders;
pub( crate ) mod helpers;
pub( crate ) mod setup;
pub( crate ) mod framebuffer;
pub( crate ) mod clip;
pub( crate ) mod primitives;
pub( crate ) mod text;
pub( crate ) mod image;
mod raii;

// ─── Public types ────────────────────────────────────────────────────────────

/// Which GLES profile the active context is. Stored so per-frame
/// fast-paths can be selected without re-querying GL.
#[ derive( Clone, Copy, PartialEq, Eq, Debug ) ]
pub enum GlesVersion
{
	V2,
	V3,
}

/// Borrowed view of the texture backing a [`GlesCanvas`].
///
/// The texture and framebuffer remain owned by ltk. Consumers may
/// sample the texture while the canvas is alive, but must not delete
/// or take ownership of the GL names. A resize can replace both
/// names, so callers should query this after rendering/resizing, not
/// cache it indefinitely.
#[ derive( Clone, Copy, Debug ) ]
pub struct BorrowedGlesTexture
{
	/// Native GL texture name for the canvas color attachment.
	pub texture_id:       u32,
	/// Native GL framebuffer name that owns `texture_id` as color attachment 0.
	pub framebuffer_id:   u32,
	/// Glow texture handle for callers already using glow.
	pub texture:          glow::Texture,
	/// Glow framebuffer handle for callers already using glow.
	pub framebuffer:      glow::Framebuffer,
	/// Texture width in physical pixels.
	pub width:            u32,
	/// Texture height in physical pixels.
	pub height:           u32,
	/// The texture contains RGBA pixels with premultiplied alpha.
	pub premultiplied:    bool,
	/// Whether consumers should treat the texture as vertically inverted.
	pub y_inverted:       bool,
}

/// Cached glyph: rasterized bitmap placed in the shared atlas.
pub ( super ) struct GlyphEntry
{
	pub ( super ) metrics:  fontdue::Metrics,
	pub ( super ) tex_w:    i32,
	pub ( super ) tex_h:    i32,
	pub ( super ) atlas_x:  u32,
	pub ( super ) atlas_y:  u32,
}

/// Cache key for the GLES glyph atlas — the GPU-side mirror of the
/// software canvas's `GlyphKey`. `glyph_id` is the per-font index
/// returned by HarfBuzz shaping; `size_bits` is `f32::to_bits` of
/// `size * dpi_scale`; `font_id` is the address of the
/// `Arc<fontdue::Font>` used for rasterisation.
pub( super ) type GlyphAtlasKey = ( u16, u32, usize );

pub( super ) const ATLAS_SIZE: u32 = 2048;

// ─── GlesCanvas ──────────────────────────────────────────────────────────────

/// GPU-accelerated canvas using EGL + GLES2/3.
///
/// Renders into a persistent FBO (the "shadow canvas") so widget
/// pixels survive across frames — this mirrors the software pixmap
/// model and is what enables the partial-redraw path on the GPU side.
/// `Self::present` blits the FBO onto the default framebuffer; the
/// caller is responsible for the `eglSwapBuffers` that follows.
pub struct GlesCanvas
{
	pub gl:           Arc<glow::Context>,
	pub version:      GlesVersion,
	/// Default font loaded from the system via `system_fonts::default_handle`.
	/// Kept as a fallback for callers that do not route through the
	/// theme registry.
	pub font:         Arc<Font>,
	/// Raw bytes of the default font. Required by rustybuzz for
	/// HarfBuzz shaping (see the `text_shaping` private module). Kept on the
	/// canvas so the shape pipeline has direct access without a
	/// global lookup.
	pub font_bytes:   Arc<Vec<u8>>,
	/// TTC sub-face index for the default font (0 for non-`.ttc` files).
	pub font_face:    u32,
	/// Optional theme font registry. Populated by the runtime after
	/// theme load; until then it is `None` and [`Self::font_for`]
	/// falls back to [`Self::font`].
	pub font_registry: Option<Arc<FontRegistry>>,
	pub dpi_scale:    f32,
	/// Layout-resolution viewport inherited from the parent canvas.
	/// `None` on a root canvas (resolve against own size); sub-canvases
	/// carry the root surface size so fluid geometry and fonts resolve
	/// the same inside offscreen content (scroll viewports, clip layers)
	/// as outside it.
	pub( crate ) layout_viewport: Option<( f32, f32 )>,
	/// Canvas-local pixel density for `Dp` resolution, when the owning
	/// surface sits on an output whose density differs from the process
	/// [`crate::density`]. `None` falls back to the global. Inherited by
	/// sub-canvases.
	pub( crate ) density: Option<f32>,
	pub global_alpha: f32,
	pub width:        u32,
	pub height:       u32,

	// Shader programs. `Program` is a Copy handle; sub-canvases share
	// these with their parent (no reference counting needed since they
	// outlive the process).
	rect_program:     glow::Program,
	tex_program:      glow::Program,
	glyph_program:    glow::Program,
	blit_program:     glow::Program,
	sub_blit_program: glow::Program,
	linear_gradient_program: glow::Program,
	radial_gradient_program: glow::Program,
	shadow_outer_program: glow::Program,
	shadow_inset_program: glow::Program,

	// Shared geometry (a unit quad as two triangles)
	quad_vao:      glow::VertexArray,
	_quad_vbo:     glow::Buffer,

	// Uniform locations for rect shader
	u_rect_mvp:    glow::UniformLocation,
	u_rect_color:  glow::UniformLocation,
	u_rect_size:   glow::UniformLocation,
	u_rect_radii:  glow::UniformLocation,
	u_rect_stroke: glow::UniformLocation,
	u_rect_pad:    glow::UniformLocation,

	// Uniform locations for texture shader
	u_tex_mvp:     glow::UniformLocation,
	u_tex_opacity: glow::UniformLocation,
	u_tex_sampler: glow::UniformLocation,

	// Uniform locations for glyph shader
	u_glyph_mvp:       glow::UniformLocation,
	u_glyph_color:     glow::UniformLocation,
	u_glyph_opacity:   glow::UniformLocation,
	u_glyph_sampler:   glow::UniformLocation,
	u_glyph_uv_offset: glow::UniformLocation,
	u_glyph_uv_scale:  glow::UniformLocation,

	glyph_batch_program: glow::Program,
	u_glyph_batch_color:   glow::UniformLocation,
	u_glyph_batch_opacity: glow::UniformLocation,
	u_glyph_batch_sampler: glow::UniformLocation,
	pub ( super ) glyph_batch_vao: glow::VertexArray,
	pub ( super ) glyph_batch_vbo: glow::Buffer,

	// Uniform location for blit shader
	u_blit_sampler: glow::UniformLocation,

	// Uniform locations for sub-canvas blit shader
	u_subblit_mvp:         glow::UniformLocation,
	u_subblit_sampler:     glow::UniformLocation,
	u_subblit_opacity:     glow::UniformLocation,
	u_subblit_fade_bottom: glow::UniformLocation,
	u_subblit_height_px:   glow::UniformLocation,

	// Uniform locations for the linear gradient shader
	u_lingrad_mvp:             glow::UniformLocation,
	u_lingrad_lut:             glow::UniformLocation,
	u_lingrad_dir:             glow::UniformLocation,
	u_lingrad_size:            glow::UniformLocation,
	u_lingrad_line_length:     glow::UniformLocation,
	u_lingrad_radii:           glow::UniformLocation,
	u_lingrad_pad:             glow::UniformLocation,
	u_lingrad_lut_domain_min:  glow::UniformLocation,
	u_lingrad_lut_domain_span: glow::UniformLocation,

	// Uniform locations for the radial gradient shader
	u_radgrad_mvp:             glow::UniformLocation,
	u_radgrad_lut:             glow::UniformLocation,
	u_radgrad_center:          glow::UniformLocation,
	u_radgrad_radius_frac:     glow::UniformLocation,
	u_radgrad_size:            glow::UniformLocation,
	u_radgrad_radii:           glow::UniformLocation,
	u_radgrad_pad:             glow::UniformLocation,
	u_radgrad_lut_domain_min:  glow::UniformLocation,
	u_radgrad_lut_domain_span: glow::UniformLocation,

	// Uniform locations for the outer shadow shader
	u_shadow_mvp:     glow::UniformLocation,
	u_shadow_size:    glow::UniformLocation,
	u_shadow_padding: glow::UniformLocation,
	u_shadow_radii:   glow::UniformLocation,
	u_shadow_spread:  glow::UniformLocation,
	u_shadow_sigma:   glow::UniformLocation,
	u_shadow_color:   glow::UniformLocation,

	// Uniform locations for the inner (inset) shadow shader
	u_inset_mvp:     glow::UniformLocation,
	u_inset_size:    glow::UniformLocation,
	u_inset_padding: glow::UniformLocation,
	u_inset_radii:   glow::UniformLocation,
	u_inset_spread:  glow::UniformLocation,
	u_inset_sigma:   glow::UniformLocation,
	u_inset_offset:  glow::UniformLocation,
	u_inset_color:   glow::UniformLocation,

	/// Inset-shadow shader variant for `BlendMode::Overlay`. Distinct
	/// from [`Self::shadow_inset_program`] because CSS Overlay cannot
	/// be expressed with fixed-function blend — this shader samples
	/// the just-snapshotted FBO content (via `aux_a`) and
	/// computes the per-channel Overlay formula in-shader, then
	/// outputs premultiplied.
	shadow_inset_overlay_program: glow::Program,
	u_inset_ov_mvp:          glow::UniformLocation,
	u_inset_ov_size:         glow::UniformLocation,
	u_inset_ov_padding:      glow::UniformLocation,
	u_inset_ov_radii:        glow::UniformLocation,
	u_inset_ov_spread:       glow::UniformLocation,
	u_inset_ov_sigma:        glow::UniformLocation,
	u_inset_ov_offset:       glow::UniformLocation,
	u_inset_ov_color:        glow::UniformLocation,
	u_inset_ov_snapshot:     glow::UniformLocation,
	u_inset_ov_canvas_size:  glow::UniformLocation,

	/// Horizontal pass of the separable Gaussian used by
	/// [`Self::fill_backdrop`](framebuffer). Samples
	/// `aux_a` (snapshot of the main FBO) and writes the
	/// horizontally-blurred result into `aux_b`.
	backdrop_blur_h_program:  glow::Program,
	u_bd_h_source:      glow::UniformLocation,
	u_bd_h_texel:       glow::UniformLocation,
	u_bd_h_canvas_size: glow::UniformLocation,
	u_bd_h_sigma:       glow::UniformLocation,

	/// Vertical pass of the separable Gaussian combined with the SDF
	/// clip to the surface shape and optional tint. Reads
	/// `aux_b` (H-blurred) and writes to the main FBO at the
	/// surface rect.
	backdrop_composite_program: glow::Program,
	u_bd_c_mvp:         glow::UniformLocation,
	u_bd_c_source:      glow::UniformLocation,
	u_bd_c_canvas_size: glow::UniformLocation,
	u_bd_c_texel:       glow::UniformLocation,
	u_bd_c_sigma:       glow::UniformLocation,
	u_bd_c_size:        glow::UniformLocation,
	u_bd_c_padding:     glow::UniformLocation,
	u_bd_c_radii:       glow::UniformLocation,
	u_bd_c_tint:        glow::UniformLocation,

	/// Fast (low-quality) horizontal Gaussian. Same role as
	/// `backdrop_blur_h_program` but with a 9-tap kernel
	/// (`RADIUS = 4`) instead of 41 taps. Used during animations /
	/// drags via [`crate::render::low_quality_paint`].
	backdrop_fast_blur_h_program:  glow::Program,
	u_bd_fh_source:      glow::UniformLocation,
	u_bd_fh_texel:       glow::UniformLocation,
	u_bd_fh_canvas_size: glow::UniformLocation,
	u_bd_fh_sigma:       glow::UniformLocation,

	/// Fast (low-quality) vertical + SDF + tint composite. 9-tap
	/// kernel counterpart to `backdrop_composite_program`.
	backdrop_fast_composite_program: glow::Program,
	u_bd_fc_mvp:         glow::UniformLocation,
	u_bd_fc_source:      glow::UniformLocation,
	u_bd_fc_canvas_size: glow::UniformLocation,
	u_bd_fc_texel:       glow::UniformLocation,
	u_bd_fc_sigma:       glow::UniformLocation,
	u_bd_fc_size:        glow::UniformLocation,
	u_bd_fc_padding:     glow::UniformLocation,
	u_bd_fc_radii:       glow::UniformLocation,
	u_bd_fc_tint:        glow::UniformLocation,

	atlas_texture:    glow::Texture,
	/// Upload format of `atlas_texture`. On GLES3 we use the modern
	/// single-channel `GL_RED` over `GL_R8`; on GLES2 we keep the
	/// legacy `GL_LUMINANCE` because `GL_R8` / `GL_RED` did not exist
	/// before ES3. The fragment shader samples `.r` and is identical
	/// for both — `GL_LUMINANCE` replicates the single channel into
	/// `.r=.g=.b`, so `.r` carries the coverage either way.
	pub ( super ) atlas_format: u32,
	atlas_cursor_x:   u32,
	atlas_cursor_y:   u32,
	atlas_row_height: u32,

	// (glyph_id, size_key, font_id) → GlyphEntry. `glyph_id` is the
	// per-font glyph index returned by HarfBuzz shaping; the cache
	// therefore persists Arabic / Devanagari / CJK shaped forms
	// independently of the source codepoints.
	glyph_cache:   HashMap<GlyphAtlasKey, GlyphEntry>,

	// Reusable texture cache for images. Keyed by
	// `(width, height, content fingerprint)` rather than the source
	// buffer's heap address — pointer-keying produced ghosting when
	// short-lived `Arc<Vec<u8>>` buffers got dropped and the
	// allocator handed the same address to a different buffer next
	// frame (the cache would happily serve the stale texture).
	// Content-keying tolerates that case at the cost of one
	// `DefaultHasher` pass over the bytes per draw call — fast for
	// any reasonable icon size. Bounded by `image.rs`'s
	// `IMAGE_CACHE_MAX_BYTES` with LRU eviction.
	image_cache:   HashMap<(u32, u32, u64), (glow::Texture, u32, u32)>,
	// LRU order of `image_cache` keys, least-recent at the front.
	image_cache_lru:   VecDeque<(u32, u32, u64)>,
	// Estimated GPU bytes held by `image_cache` (RGBA8: w × h × 4).
	image_cache_bytes: usize,

	// Gradient LUT cache: FNV-ish hash of the 512×RGBA8 LUT bytes → texture.
	// Gradients are theme-derived and constant across frames; caching avoids
	// a glTexImage2D round-trip (create + upload + delete) on every draw call.
	// Cleared via `clear_gradient_cache()` on theme changes.
	gradient_lut_cache: HashMap<u64, glow::Texture>,

	/// Current scissor: `Some(rect)` when a clip is installed
	/// (GL_SCISSOR_TEST is enabled), `None` when cleared.
	clip_scissor:  Option<Rect>,

	/// Anti-aliased path clip (`set_clip_path`). While `clip_layer_active`,
	/// `activate_target` redirects draws to `clip_layer` (a full-canvas
	/// offscreen FBO); ending the clip composites that layer back onto `fbo`
	/// multiplied by `clip_mask_tex` (an anti-aliased coverage texture covering
	/// `clip_bbox`). The layer FBO is allocated lazily on the first path clip.
	clip_layer:        Option<( glow::Framebuffer, glow::Texture )>,
	clip_layer_active: bool,
	clip_mask_tex:     Option<glow::Texture>,
	clip_bbox:         Rect,
	/// Composite program (layer × coverage mask). Shared with sub-canvases.
	clip_composite_program: glow::Program,
	u_clip_mvp:        glow::UniformLocation,
	u_clip_layer:      glow::UniformLocation,
	u_clip_mask:       glow::UniformLocation,
	u_clip_canvas:     glow::UniformLocation,
	u_clip_bbox:       glow::UniformLocation,

	/// Persistent shadow framebuffer. All draw methods bind this;
	/// [`Self::present`](framebuffer) is the only call that switches
	/// to the default framebuffer.
	fbo:     glow::Framebuffer,
	/// Color attachment for `fbo`. Reallocated on resize.
	fbo_tex: glow::Texture,

	/// Auxiliary FBO + texture used as a snapshot of `fbo`
	/// for framebuffer-fetch-style effects (CSS `Overlay` blend,
	/// backdrop blur). Lazily allocated on first use; dropped on
	/// resize so the next user re-allocates at the new size.
	aux_a: Option<( glow::Framebuffer, glow::Texture )>,
	/// Second auxiliary FBO, used as ping-pong target for the
	/// separable Gaussian blur in backdrop compositing. Uses LINEAR
	/// filtering for the V-pass bilinear sampling (vs `aux_a`'s
	/// NEAREST).
	aux_b: Option<( glow::Framebuffer, glow::Texture )>,
}

// ─── Drop ────────────────────────────────────────────────────────────────────

/// Free this canvas's owned GL resources: FBO, color attachment, aux
/// FBOs if allocated, and any cached glyph / image textures. Shader
/// programs and the quad VAO/VBO are shared with sub-canvases and
/// intentionally NOT deleted here — they leak at process exit, which
/// is fine for a process-wide GL context.
impl Drop for GlesCanvas
{
	fn drop( &mut self )
	{
		// SAFETY: every handle freed below was created through `self.gl` —
		// either in `setup.rs::new` / `sub_canvas` (`fbo`, `fbo_tex`),
		// `framebuffer.rs::ensure_aux_a` / `ensure_aux_b` (`aux_a`, `aux_b`),
		// `text.rs::draw_text` (`glyph_cache`), `image.rs::draw_image_data`
		// (`image_cache`), or `primitives.rs::ensure_lut_texture`
		// (`gradient_lut_cache`). Each container `drain` / `take` is
		// consumed once so no double-free is possible. Caller must keep
		// the GL context current at drop time — this is documented on
		// `core::UiSurface::from_current_gles_loader` and
		// `from_canvas_with_egl_context`.
		unsafe
		{
			self.gl.delete_framebuffer( self.fbo );
			self.gl.delete_texture( self.fbo_tex );
			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 );
			}
			if let Some( ( fbo, tex ) ) = self.aux_a.take()
			{
				self.gl.delete_framebuffer( fbo );
				self.gl.delete_texture( tex );
			}
			if let Some( ( fbo, tex ) ) = self.aux_b.take()
			{
				self.gl.delete_framebuffer( fbo );
				self.gl.delete_texture( tex );
			}
			self.glyph_cache.clear();
			self.gl.delete_texture( self.atlas_texture );
			for ( _, ( tex, _, _ ) ) in self.image_cache.drain()
			{
				self.gl.delete_texture( tex );
			}
			for ( _, tex ) in self.gradient_lut_cache.drain()
			{
				self.gl.delete_texture( tex );
			}
		}
	}
}
