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

//! Rendering surface used by every widget.
//!
//! [`Canvas`] is a thin enum wrapper over the per-frame rendering
//! backend. The CPU backend is [`SoftwareCanvas`] (tiny-skia + fontdue
//! rasterised into a `Pixmap`). The GPU backend is
//! [`crate::gles_render::GlesCanvas`] (EGL + GLES2/3).
//!
//! Widgets only ever see `&mut Canvas` — they call `fill_rect`,
//! `draw_text`, etc. The enum dispatches by `match self` (no `dyn`,
//! so the call sites stay monomorphic and inlinable). Field-style
//! access to backend internals (`pixmap`, `font`, `dpi_scale`…) is
//! replaced by accessor methods that the GPU variant can also
//! implement.
//!
//! # Submodule layout
//!
//! * [`setup`] — `SoftwareCanvas::{new, sub_canvas, resize, blit,
//!   set_font_registry, font_for}` (construction + accessors).
//! * [`clip`] — `SoftwareCanvas::{set_clip_rects, set_clip_path,
//!   clear_clip, has_clip, strip_intersects_clip,
//!   clear_rects_transparent}`.
//! * [`primitives`] — `SoftwareCanvas::{clear, fill, fill_rect,
//!   stroke_rect, draw_line}`.
//! * [`text`] — `SoftwareCanvas::{draw_text, measure_text}`.
//! * [`image`] — `SoftwareCanvas::{draw_image_data,
//!   write_to_wayland_buf}`.
//! * [`helpers`] — free functions: `build_ts_path`,
//!   `build_rounded_rect`. System-font resolution lives in
//!   `crate::system_fonts`.

use std::cell::Cell;
use std::sync::Arc;

use fontdue::{ Font, LineMetrics, Metrics };
use tiny_skia::{ Mask, Pixmap };

use crate::gles_render::{ BorrowedGlesTexture, GlesCanvas, GlesVersion };
use crate::theme::{ FontRegistry, FontStyle, InsetShadow, Paint as ThemePaint, Shadow };
use crate::types::{ Color, Corners, Length, Rect, WidgetScaling };

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

// ─── Backend flag ────────────────────────────────────────────────────────────

thread_local!
{
	/// `true` when this thread's surfaces are rendered through the
	/// software (tiny-skia / SHM) path, `false` when they go through
	/// the GLES path. Set once at startup based on EGL availability
	/// and read by view code that needs to branch on backend (e.g. a
	/// layout that costs something specific to one path and isn't
	/// worth replicating on the other). Stays a thread-local so view
	/// code does not need to plumb a flag through every layout call.
	static SOFTWARE_RENDER: Cell<bool> = const { Cell::new( false ) };
}

/// Toggle the software-render flag for this thread. Consumers read
/// with [`is_software_render`].
pub fn set_software_render( on: bool )
{
	SOFTWARE_RENDER.with( | c | c.set( on ) );
}

/// `true` when the active surfaces on this thread render through the
/// software path. Used by view code that wants to avoid pipeline
/// effects the software backend doesn't implement.
pub fn is_software_render() -> bool
{
	SOFTWARE_RENDER.with( | c | c.get() )
}

// ─── Glyph cache ─────────────────────────────────────────────────────────────

/// Cache key for a rasterized glyph. `size_bits` is the f32 bit
/// pattern of `size * dpi_scale`; `font_id` is the address of the
/// `Arc<Font>` used for the rasterisation, so distinct weights /
/// families of the same `(glyph_id, size)` do not collide on the
/// cache. `glyph_id` is the per-font glyph index returned by
/// HarfBuzz shaping, so cached entries persist across script
/// transitions and Arabic / Devanagari / CJK forms cluster
/// independently of the `char` codepoint that produced them.
#[ derive( Hash, PartialEq, Eq, Clone, Copy ) ]
pub ( super ) struct GlyphKey
{
	pub ( super ) glyph_id: u16,
	pub ( super ) size_bits: u32,
	pub ( super ) font_id:   usize,
}

/// Cached glyph bitmap and metrics. Fontdue's rasterize call is the
/// dominant per-frame CPU cost for text-heavy UIs; reusing across
/// frames avoids that work.
pub ( super ) struct GlyphEntry
{
	pub ( super ) metrics: Metrics,
	pub ( super ) bitmap:  Vec<u8>,
}

// ─── SoftwareCanvas ──────────────────────────────────────────────────────────

/// Software rendering backend backed by a tiny-skia [`Pixmap`] and a
/// fontdue [`Font`].
///
/// Wrapped by [`Canvas`] so the GPU backend can be slotted in by the
/// runtime without changing widget code. Widgets themselves never see
/// `SoftwareCanvas` directly.
pub struct SoftwareCanvas
{
	/// The pixel buffer drawn into each frame.
	pub pixmap:    Pixmap,
	/// The loaded system font used for all text rendering.
	///
	/// Kept as the default fallback so widgets that do not yet ask for a
	/// specific family through [`SoftwareCanvas::font_for`] keep
	/// working. Populated from
	/// `crate::system_fonts::default_handle` at construction time.
	pub font:      Arc<Font>,
	/// Raw bytes of the default font. Kept alongside `font` so the
	/// HarfBuzz shaper (rustybuzz) can be invoked without re-reading
	/// the file — fontdue does not expose its internal byte buffer.
	pub font_bytes: Arc<Vec<u8>>,
	/// TTC sub-face index for the default font (0 for single-face
	/// files; collection index for `.ttc` archives).
	pub font_face:  u32,
	/// Optional theme font registry. When present,
	/// [`SoftwareCanvas::font_for`] consults it before falling back
	/// to `font`. Populated by the caller once the theme's `fonts`
	/// block has been loaded.
	pub font_registry: Option<Arc<FontRegistry>>,
	/// DPI scale factor applied to font sizes.
	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>,
	/// Global alpha multiplier for all drawing operations (0.0 =
	/// transparent, 1.0 = opaque).
	pub global_alpha: f32,
	/// Persistent cache of rasterized glyphs, indexed by (char, scaled size).
	/// Grows on demand; not LRU-bounded since typical UIs use few sizes.
	glyph_cache: std::collections::HashMap<GlyphKey, GlyphEntry>,
	/// Optional clip mask applied to all paint operations. Set via
	/// [`Canvas::set_clip_rects`] during a partial redraw so only
	/// pixels inside the dirty rects are touched. `None` means "draw
	/// everywhere".
	clip_mask:     Option<Mask>,
	/// Bounding boxes of the clip rects in physical pixels. Used by
	/// [`SoftwareCanvas::draw_text`] to do an early reject without
	/// poking the mask byte by byte (the Mask buffer is still
	/// authoritative inside the pixel loop).
	clip_bounds:   Vec<Rect>,
}

// ─── Canvas enum + dispatch ─────────────────────────────────────────────────

/// Per-frame rendering surface. Wraps a backend (software or GPU)
/// behind an enum so widgets can stay backend-agnostic.
///
/// All drawing methods are dispatched by `match self` — no `dyn`
/// indirection, so the backend branch stays predictable and
/// inlinable in the hot path.
pub enum Canvas
{
	/// CPU rasterisation via tiny-skia + fontdue, written to a
	/// `wl_shm` buffer.
	Software( SoftwareCanvas ),
	/// GPU rasterisation via EGL + GLES 2/3. Presents via
	/// `eglSwapBuffers`; [`Canvas::write_to_wayland_buf`] is a no-op
	/// for this variant.
	Gles( GlesCanvas ),
}

impl Canvas
{
	/// Build a software canvas. The GPU backend requires an EGL
	/// context — see [`Canvas::new_gles`].
	pub fn new( width: u32, height: u32 ) -> Self
	{
		Canvas::Software( SoftwareCanvas::new( width, height ) )
	}

	/// Build a GPU canvas on an already-current EGL context.
	pub fn new_gles(
		gl: Arc<glow::Context>, version: GlesVersion, width: u32, height: u32,
	) -> Self
	{
		Canvas::Gles( GlesCanvas::new( gl, version, width, height ) )
	}

	/// `(width, height)` of the underlying surface in physical pixels.
	pub fn size( &self ) -> ( u32, u32 )
	{
		match self
		{
			Canvas::Software( c ) => ( c.pixmap.width(), c.pixmap.height() ),
			Canvas::Gles( c )     => c.size(),
		}
	}

	/// `(width, height)` of the surface in **logical** pixels (physical
	/// size divided by `dpi_scale`). This is the right viewport for
	/// resolving [`crate::Length`] values, which are themselves in
	/// logical units. Falls back to physical size if `dpi_scale` is
	/// 0 or negative so a misconfigured canvas still returns a usable
	/// non-zero viewport instead of `NaN`/`inf`.
	///
	/// ```rust,no_run
	/// # use ltk::core::Canvas;
	/// let mut c = Canvas::new( 720, 1440 );
	/// c.set_dpi_scale( 2.0 );
	/// assert_eq!( c.viewport_logical(), ( 360.0, 720.0 ) );
	/// ```
	pub fn viewport_logical( &self ) -> ( f32, f32 )
	{
		let ( pw, ph ) = match self.layout_viewport()
		{
			Some( ( w, h ) ) => ( w, h ),
			None =>
			{
				let ( w, h ) = self.size();
				( w as f32, h as f32 )
			}
		};
		let scale = self.dpi_scale();
		if scale > 0.0
		{
			( pw / scale, ph / scale )
		} else {
			( pw, ph )
		}
	}

	/// `(width, height)` of the surface in **physical** pixels — the same
	/// space the layout tree is computed in (the root rect is `pw × ph`).
	/// This is the viewport that layout-affecting [`crate::Length`] values
	/// (widths, paddings, gaps, widget sizes) must resolve against so a
	/// `Vw(100)` fills the surface. Text font sizes are the exception:
	/// they resolve against [`Self::viewport_logical`] and are then scaled
	/// by `dpi_scale` at raster time, so they must NOT use this.
	pub fn viewport_layout( &self ) -> ( f32, f32 )
	{
		if let Some( v ) = self.layout_viewport() { return v; }
		let ( pw, ph ) = self.size();
		( pw as f32, ph as f32 )
	}

	/// Inherited layout viewport, when this is a sub-canvas.
	fn layout_viewport( &self ) -> Option<( f32, f32 )>
	{
		match self
		{
			Canvas::Software( c ) => c.layout_viewport,
			Canvas::Gles( c )     => c.layout_viewport,
		}
	}

	/// Pixel density used to resolve [`crate::LengthBase::Dp`] values on
	/// this canvas: the canvas-local density when one was pinned with
	/// [`Self::set_density`], the process [`crate::density`] otherwise.
	pub fn density( &self ) -> f32
	{
		let local = match self
		{
			Canvas::Software( c ) => c.density,
			Canvas::Gles( c )     => c.density,
		};
		local.unwrap_or_else( crate::types::density )
	}

	/// Pin this canvas — and every sub-canvas later derived from it — to
	/// a pixel density, overriding the process [`crate::density`] for
	/// `Dp` resolution. For a surface sitting on an output whose DPI
	/// differs from the one the process global was derived from.
	pub fn set_density( &mut self, d: f32 )
	{
		let d = d.max( 0.0 );
		match self
		{
			Canvas::Software( c ) => c.density = Some( d ),
			Canvas::Gles( c )     => c.density = Some( d ),
		}
	}

	/// Pin this canvas' layout viewport to its own pixel size, opting a
	/// sub-canvas out of the root-viewport inheritance: viewport-relative
	/// and fluid [`Length`]s drawn on it resolve against the canvas rect
	/// itself. For fixed-size floating mini-UIs (a phone-shaped panel on
	/// a desktop surface) whose content must not scale with the surface
	/// hosting them.
	pub( crate ) fn set_local_layout_viewport( &mut self )
	{
		let ( w, h ) = self.size();
		match self
		{
			Canvas::Software( c ) => c.layout_viewport = Some( ( w as f32, h as f32 ) ),
			Canvas::Gles( c )     => c.layout_viewport = Some( ( w as f32, h as f32 ) ),
		}
	}

	/// Resolve an explicit [`Length`] in **geometry** space: against
	/// [`Self::viewport_layout`], with this canvas' [`Self::density`].
	/// Widgets resolve caller-supplied geometry lengths through this so
	/// a `Length::dp` override follows the canvas the widget draws on.
	pub fn resolve_geom( &self, l: Length ) -> f32
	{
		l.resolve_with_density( self.viewport_layout(), Length::EM_BASE_DEFAULT, self.density() )
	}

	/// Resolve an explicit [`Length`] in **font** space: against
	/// [`Self::viewport_logical`], with this canvas' [`Self::density`],
	/// times the global accessibility [`crate::text_scale`]. Counterpart
	/// of [`Self::resolve_geom`] for font sizes, which are handed to the
	/// raster path pre-`dpi_scale`.
	pub fn resolve_font( &self, l: Length ) -> f32
	{
		l.resolve_with_density( self.viewport_logical(), Length::EM_BASE_DEFAULT, self.density() )
			* crate::types::text_scale()
	}

	/// Resolve a stock-widget **geometry** design pixel (height, padding,
	/// box size, gap…) through the process-wide [`crate::WidgetScaling`]
	/// mode, into a concrete physical-pixel value for the layout tree.
	/// Widgets call this instead of using a raw `theme::` constant so their
	/// intrinsic geometry follows the app's chosen adaptation strategy
	/// ([`crate::WidgetScaling::Fluid`] → surface-proportional,
	/// [`crate::WidgetScaling::Physical`] → constant physical size). Geometry
	/// lives in physical space, so this resolves against
	/// [`Self::viewport_layout`].
	pub fn geom_px( &self, design_px: f32 ) -> f32
	{
		self.resolve_geom( Length::widget( design_px ) )
	}

	/// Resolve a stock-widget **font** design pixel through the process-wide
	/// [`crate::WidgetScaling`] mode. Font sizes are handed to the raster
	/// path pre-`dpi_scale`, so this bridges the logical/physical split for
	/// each mode: in [`crate::WidgetScaling::Fluid`] it resolves the fluid
	/// size against [`Self::viewport_logical`] (so `× dpi_scale` at raster
	/// yields a surface-proportional physical size); in
	/// [`crate::WidgetScaling::Physical`] it divides the density-scaled size
	/// by `dpi_scale` (so `× dpi_scale` yields a constant physical size).
	pub fn font_px( &self, design_px: f32 ) -> f32
	{
		match crate::types::widget_scaling()
		{
			WidgetScaling::Fluid =>
			{
				self.resolve_font( Length::fluid( design_px ) )
			}
			WidgetScaling::Physical =>
			{
				let scale = self.dpi_scale();
				let scale = if scale > 0.0 { scale } else { 1.0 };
				design_px * self.density() / scale * crate::types::text_scale()
			}
		}
	}

	/// Borrow the GLES texture backing this canvas, when the canvas
	/// is GPU-backed.
	pub fn borrowed_gles_texture( &self ) -> Option<BorrowedGlesTexture>
	{
		match self
		{
			Canvas::Software( _ ) => None,
			Canvas::Gles( c )     => Some( c.borrowed_texture() ),
		}
	}

	/// Read a GLES canvas into tightly packed RGBA8, top-left row
	/// first. Intentionally unavailable for software canvases because
	/// the software backend's canonical export path is
	/// [`Self::write_to_wayland_buf`].
	pub fn read_gles_rgba_pixels( &self, out: &mut [u8] ) -> Result<(), String>
	{
		match self
		{
			Canvas::Software( _ ) => Err( "read_gles_rgba_pixels requires Canvas::Gles".to_string() ),
			Canvas::Gles( c )     => c.read_rgba_pixels( out ),
		}
	}

	/// Whether this is the CPU (software) backend. Callers that can honour a real
	/// path clip on the software backend but only a bounding rect on GLES branch
	/// on this.
	pub fn is_software( &self ) -> bool
	{
		matches!( self, Canvas::Software( _ ) )
	}

	/// Read this canvas into tightly packed straight-alpha RGBA8, top-left row
	/// first (`out.len()` must be at least width*height*4). Unlike
	/// [`Self::read_gles_rgba_pixels`] this also serves the software backend, by
	/// un-premultiplying its pixmap — used to read back an offscreen software
	/// canvas (e.g. an Android `Canvas(Bitmap)`) into a straight-alpha buffer.
	pub fn read_rgba_pixels( &self, out: &mut [u8] ) -> Result<(), String>
	{
		match self
		{
			Canvas::Software( c ) =>
			{
				let pixels = c.pixmap.pixels();
				if out.len() < pixels.len() * 4
				{
					return Err( "read_rgba_pixels: output buffer too small".to_string() );
				}
				for ( i, p ) in pixels.iter().enumerate()
				{
					let d = p.demultiply();
					let o = i * 4;
					out[ o ]     = d.red();
					out[ o + 1 ] = d.green();
					out[ o + 2 ] = d.blue();
					out[ o + 3 ] = d.alpha();
				}
				Ok( () )
			}
			Canvas::Gles( c ) => c.read_rgba_pixels( out ),
		}
	}

	/// Composite an externally-owned GL texture into `dest`. No-op on
	/// the software backend (no GL state to sample from). Used by
	/// widgets that host content rendered by an external producer —
	/// the producer keeps ownership of the texture name; this call
	/// only samples it through the standard texture program.
	pub fn draw_external_texture( &mut self, texture: glow::Texture, dest: Rect, opacity: f32 )
	{
		match self
		{
			Canvas::Software( _ ) => {}
			Canvas::Gles( c )     => c.draw_external_texture( texture, dest, opacity ),
		}
	}

	pub fn dpi_scale( &self ) -> f32
	{
		match self
		{
			Canvas::Software( c ) => c.dpi_scale,
			Canvas::Gles( c )     => c.dpi_scale(),
		}
	}

	pub fn set_dpi_scale( &mut self, s: f32 )
	{
		match self
		{
			Canvas::Software( c ) => c.dpi_scale = s,
			Canvas::Gles( c )     => c.set_dpi_scale( s ),
		}
	}

	pub fn global_alpha( &self ) -> f32
	{
		match self
		{
			Canvas::Software( c ) => c.global_alpha,
			Canvas::Gles( c )     => c.global_alpha(),
		}
	}

	pub fn set_global_alpha( &mut self, a: f32 )
	{
		match self
		{
			Canvas::Software( c ) => c.global_alpha = a,
			Canvas::Gles( c )     => c.set_global_alpha( a ),
		}
	}

	/// Shared font handle. Exposed so widgets that need raw `fontdue`
	/// access (e.g. `Text` for ascent/descent) do not have to go
	/// through wrappers for every metric they read.
	pub fn font( &self ) -> &Font
	{
		match self
		{
			Canvas::Software( c ) => &c.font,
			Canvas::Gles( c )     => c.font(),
		}
	}

	/// Install a theme font registry on the active backend.
	pub fn set_font_registry( &mut self, registry: Arc<FontRegistry> )
	{
		// Drop the shaped-line cache: a new registry can swap the faces the
		// resolver leads with, so cached glyph runs may no longer match.
		crate::text_shaping::clear_shape_cache();
		match self
		{
			Canvas::Software( c ) => c.set_font_registry( registry ),
			Canvas::Gles( c )     => c.set_font_registry( registry ),
		}
	}

	/// Resolve a specific font via the theme registry, falling back
	/// to the system-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>
	{
		match self
		{
			Canvas::Software( c ) => c.font_for( family, weight, style ),
			Canvas::Gles( c )     => c.font_for( family, weight, style ),
		}
	}

	/// Convenience wrapper around `font().metrics(...)` already
	/// pre-scaled by `dpi_scale`. Most callers want this rather than
	/// the raw font handle.
	pub fn font_metrics( &self, ch: char, size: f32 ) -> Metrics
	{
		self.font().metrics( ch, size * self.dpi_scale() )
	}

	/// Convenience wrapper around `font().horizontal_line_metrics(...)`,
	/// pre-scaled by `dpi_scale` like [`Self::font_metrics`].
	pub fn font_line_metrics( &self, size: f32 ) -> Option<LineMetrics>
	{
		self.font().horizontal_line_metrics( size * self.dpi_scale() )
	}

	pub fn resize( &mut self, width: u32, height: u32 )
	{
		match self
		{
			Canvas::Software( c ) => c.resize( width, height ),
			Canvas::Gles( c )     => c.resize( width, height ),
		}
	}

	pub fn sub_canvas( &self, width: u32, height: u32 ) -> Canvas
	{
		match self
		{
			Canvas::Software( c ) => Canvas::Software( c.sub_canvas( width, height ) ),
			Canvas::Gles( c )     => Canvas::Gles( c.sub_canvas( width, height ) ),
		}
	}

	pub fn blit( &mut self, src: &Canvas, dest_x: i32, dest_y: i32 )
	{
		self.blit_fade_bottom( src, dest_x, dest_y, 0.0 )
	}

	/// Like [`Self::blit`] but feathers the last `fade_bottom_px` source
	/// rows so the bottom edge fades to transparent. The software backend
	/// currently ignores `fade_bottom_px`, so the dissolve is GLES-only.
	pub fn blit_fade_bottom( &mut self, src: &Canvas, dest_x: i32, dest_y: i32, fade_bottom_px: f32 )
	{
		match ( self, src )
		{
			( Canvas::Software( dst ), Canvas::Software( s ) ) =>
			{
				let _ = fade_bottom_px;
				dst.blit( s, dest_x, dest_y );
			}
			( Canvas::Gles( dst ), Canvas::Gles( s ) ) =>
			{
				dst.blit_fade_bottom( s, dest_x, dest_y, fade_bottom_px );
			}
			// Cross-backend blits would need an SHM↔texture upload.
			// The toolkit only ever creates sub-canvases of the same
			// kind as their parent, so this is unreachable in practice.
			_ => unimplemented!( "cross-backend blit not supported" ),
		}
	}

	pub fn set_clip_rects( &mut self, rects: &[Rect] )
	{
		match self
		{
			Canvas::Software( c ) => c.set_clip_rects( rects ),
			Canvas::Gles( c )     => c.set_clip_rects( rects ),
		}
	}

	/// Clip subsequent draws to an arbitrary vector path (in surface
	/// coordinates). Anti-aliased per-path clipping: the software backend uses a
	/// tiny-skia coverage mask; the GLES backend captures the clipped draws into
	/// an offscreen layer and composites it back through an anti-aliased coverage
	/// mask. Replaces any active clip; restore the previous clip via
	/// [`Self::set_clip_rects`] with
	/// a [`Self::clip_bounds`] snapshot taken beforehand, or [`Self::clear_clip`].
	pub fn set_clip_path( &mut self, cmds: &[ crate::types::PathCmd ] )
	{
		match self
		{
			Canvas::Software( c ) => c.set_clip_path( cmds ),
			Canvas::Gles( c )     => c.set_clip_path( cmds ),
		}
	}

	/// Snapshot the currently installed clip bounds (empty when no clip
	/// is active). Used by widgets that need to install a tighter clip
	/// for a single primitive and then restore whatever the outer
	/// partial-redraw or sub-canvas clip was — there is no stack
	/// internally, so round-tripping through
	/// [`Self::set_clip_rects`] with the snapshot is how to compose.
	pub fn clip_bounds( &self ) -> Vec<Rect>
	{
		match self
		{
			Canvas::Software( c ) => c.clip_bounds_snapshot(),
			Canvas::Gles( c )     => c.clip_bounds_snapshot(),
		}
	}

	pub fn clear_clip( &mut self )
	{
		match self
		{
			Canvas::Software( c ) => c.clear_clip(),
			Canvas::Gles( c )     => c.clear_clip(),
		}
	}

	pub fn clear( &mut self )
	{
		match self
		{
			Canvas::Software( c ) => c.clear(),
			Canvas::Gles( c )     => c.clear(),
		}
	}

	pub fn fill( &mut self, color: Color )
	{
		match self
		{
			Canvas::Software( c ) => c.fill( color ),
			Canvas::Gles( c )     => c.fill( color ),
		}
	}

	pub fn fill_rect( &mut self, rect: Rect, color: Color, corners: impl Into<Corners> )
	{
		let corners = corners.into();
		match self
		{
			Canvas::Software( c ) => c.fill_rect( rect, color, corners ),
			Canvas::Gles( c )     => c.fill_rect( rect, color, corners ),
		}
	}

	/// Paint-driven rectangle fill.
	///
	/// Dispatches on the [`crate::theme::Paint`] variant. Solid
	/// fills go straight through [`Self::fill_rect`]. Gradients
	/// (linear and radial) are routed to dedicated shaders on the
	/// GPU backend; on the Software backend they still collapse to a
	/// flat fill from the first stop — tiny-skia can render
	/// gradients natively, but wiring that up is left for a
	/// follow-up.
	pub fn fill_paint_rect( &mut self, rect: Rect, paint: &ThemePaint, corners: impl Into<Corners> )
	{
		let corners = corners.into();
		match paint
		{
			ThemePaint::Solid( c )  => self.fill_rect( rect, *c, corners ),
			ThemePaint::Linear( g ) =>
			{
				match self
				{
					Canvas::Software( sc ) =>
					{
						let c = g.stops.first().map( |s| s.color ).unwrap_or( Color::TRANSPARENT );
						sc.fill_rect( rect, c, corners );
					}
					Canvas::Gles( gc ) => gc.fill_linear_gradient_rect( rect, g, corners ),
				}
			}
			ThemePaint::Radial( g ) =>
			{
				match self
				{
					Canvas::Software( sc ) =>
					{
						let c = g.stops.first().map( |s| s.color ).unwrap_or( Color::TRANSPARENT );
						sc.fill_rect( rect, c, corners );
					}
					Canvas::Gles( gc ) => gc.fill_radial_gradient_rect( rect, g, corners ),
				}
			}
		}
	}

	pub fn stroke_rect( &mut self, rect: Rect, color: Color, width: f32, corners: impl Into<Corners> )
	{
		let corners = corners.into();
		match self
		{
			Canvas::Software( c ) => c.stroke_rect( rect, color, width, corners ),
			Canvas::Gles( c )     => c.stroke_rect( rect, color, width, corners ),
		}
	}

	/// Fill an arbitrary vector path (commands in surface coordinates) with a
	/// solid colour. The software backend rasterises with tiny-skia directly;
	/// the GLES backend rasterises into a tiny-skia pixmap and blits it (the
	/// CPU fallback — no path shader on the GPU path).
	pub fn fill_path( &mut self, cmds: &[ crate::types::PathCmd ], color: Color )
	{
		match self
		{
			Canvas::Software( c ) => c.fill_path( cmds, color ),
			Canvas::Gles( c )     => c.fill_path( cmds, color ),
		}
	}

	/// Stroke an arbitrary vector path (commands in surface coordinates).
	pub fn stroke_path( &mut self, cmds: &[ crate::types::PathCmd ], color: Color, width: f32 )
	{
		match self
		{
			Canvas::Software( c ) => c.stroke_path( cmds, color, width ),
			Canvas::Gles( c )     => c.stroke_path( cmds, color, width ),
		}
	}

	/// Paint an outer drop shadow behind the rounded rect `target`.
	///
	/// On the GPU backend this runs an analytic soft-shadow shader
	/// in one draw call — no FBO, no cache, no readback. On the
	/// Software backend it is a no-op today.
	pub fn fill_shadow_outer( &mut self, target: Rect, shadow: &Shadow, corners: impl Into<Corners> )
	{
		let corners = corners.into();
		match self
		{
			Canvas::Software( _ ) => { /* TODO: tiny-skia BlurDropShadow */ }
			Canvas::Gles( c )     => c.fill_shadow_outer( target, shadow, corners ),
		}
	}

	/// Paint an inner (inset) shadow inside the rounded rect
	/// `target`.
	///
	/// On the GPU backend, uses a dedicated shader whose inner SDF
	/// encodes `shadow.offset` and `shadow.spread`. The blend state
	/// is switched per-call to honour `shadow.blend`: `Normal`,
	/// `PlusLighter`, `Multiply` and `Screen` map to fixed-function
	/// blend modes; `Overlay` routes through a dedicated shader that
	/// snapshots the FBO and computes the CSS Overlay formula
	/// in-shader.
	///
	/// On the Software backend this is a no-op today.
	pub fn fill_shadow_inset( &mut self, target: Rect, shadow: &InsetShadow, corners: impl Into<Corners> )
	{
		let corners = corners.into();
		match self
		{
			Canvas::Software( _ ) => { /* TODO: tiny-skia inner shadow */ }
			Canvas::Gles( c )     => c.fill_shadow_inset( target, shadow, corners ),
		}
	}

	/// Unified surface painter. Composes a themed surface in the canonical
	/// paint order: outer shadows → fill → insets.
	pub fn fill_surface
	(
		&mut self,
		rect:           Rect,
		fill:           &ThemePaint,
		outer_shadows:  &[Shadow],
		inset_shadows:  &[InsetShadow],
		corners:        impl Into<Corners>,
	)
	{
		let corners = corners.into();

		for shadow in outer_shadows
		{
			self.fill_shadow_outer( rect, shadow, corners );
		}

		self.fill_paint_rect( rect, fill, corners );

		for inset in inset_shadows
		{
			self.fill_shadow_inset( rect, inset, corners );
		}
	}

	pub fn draw_line( &mut self, x0: f32, y0: f32, x1: f32, y1: f32, color: Color, width: f32 )
	{
		match self
		{
			Canvas::Software( c ) => c.draw_line( x0, y0, x1, y1, color, width ),
			Canvas::Gles( c )     => c.draw_line( x0, y0, x1, y1, color, width ),
		}
	}

	pub fn draw_text( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color )
	{
		match self
		{
			Canvas::Software( c ) => c.draw_text( text, x, y, size, color ),
			Canvas::Gles( c )     => c.draw_text( text, x, y, size, color ),
		}
	}

	/// Draw `text` with an explicitly supplied font instead of the
	/// canvas default. Use [`Self::font_for`] to resolve a `(family,
	/// weight, style)` triple from the active theme registry first.
	pub fn draw_text_with_font( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: &Arc<Font> )
	{
		match self
		{
			Canvas::Software( c ) => c.draw_text_with_font( text, x, y, size, color, font ),
			Canvas::Gles( c )     => c.draw_text_with_font( text, x, y, size, color, font ),
		}
	}

	pub fn measure_text( &self, text: &str, size: f32 ) -> f32
	{
		match self
		{
			Canvas::Software( c ) => c.measure_text( text, size ),
			Canvas::Gles( c )     => c.measure_text( text, size ),
		}
	}

	/// Width of `text` rendered with `font`. Mirrors
	/// [`Self::measure_text`] but bypasses the canvas default font so
	/// text laid out at one weight and drawn at another stays aligned.
	pub fn measure_text_with_font( &self, text: &str, size: f32, font: &Arc<Font> ) -> f32
	{
		match self
		{
			Canvas::Software( c ) => c.measure_text_with_font( text, size, font ),
			Canvas::Gles( c )     => c.measure_text_with_font( text, size, font ),
		}
	}

	pub fn draw_image_data( &mut self, rgba_data: &[u8], img_w: u32, img_h: u32, dest: Rect, opacity: f32 )
	{
		match self
		{
			Canvas::Software( c ) => c.draw_image_data( rgba_data, img_w, img_h, dest, opacity ),
			Canvas::Gles( c )     => c.draw_image_data( rgba_data, img_w, img_h, dest, opacity ),
		}
	}

	/// Zero pixels inside each rect — used by the partial-redraw
	/// path when the surface background is fully transparent.
	pub fn clear_rects_transparent( &mut self, rects: &[Rect] )
	{
		match self
		{
			Canvas::Software( c ) => c.clear_rects_transparent( rects ),
			Canvas::Gles( c )     => c.clear_rects_transparent( rects ),
		}
	}

	/// Copy / present the rendered frame. For software this fills a
	/// `wl_shm` buffer (with optional R/B swap for Argb8888). For
	/// GPU the commit happens via `eglSwapBuffers` elsewhere — this
	/// call is a no-op.
	pub fn write_to_wayland_buf( &self, buf: &mut [u8], swap_rb: bool )
	{
		match self
		{
			Canvas::Software( c ) => c.write_to_wayland_buf( buf, swap_rb ),
			Canvas::Gles( _ )     => {}
		}
	}

	/// Publish the in-progress GPU frame: blit the FBO onto the EGL
	/// window's default framebuffer. The follow-up `eglSwapBuffers`
	/// (done outside the canvas) is what actually commits to the
	/// compositor. No-op on software, where presentation is the SHM
	/// `attach_to`/`commit` pair.
	pub fn present( &mut self )
	{
		match self
		{
			Canvas::Software( _ ) => {}
			Canvas::Gles( c )     => c.present(),
		}
	}
}

#[ cfg( test ) ]
mod viewport_tests
{
	use super::Canvas;

	#[ test ]
	fn viewport_logical_at_scale_one_matches_physical()
	{
		let c = Canvas::new( 800, 600 );
		assert_eq!( c.viewport_logical(), ( 800.0, 600.0 ) );
	}

	#[ test ]
	fn viewport_logical_divides_by_dpi_scale()
	{
		let mut c = Canvas::new( 720, 1440 );
		c.set_dpi_scale( 2.0 );
		assert_eq!( c.viewport_logical(), ( 360.0, 720.0 ) );
	}

	#[ test ]
	fn viewport_logical_falls_back_to_physical_when_scale_is_zero()
	{
		// Guard the misconfigured-scale path: a divide-by-zero would
		// poison every `Length::Vmin`/`Vw`/`Vh` resolution downstream.
		let mut c = Canvas::new( 800, 600 );
		c.set_dpi_scale( 0.0 );
		assert_eq!( c.viewport_logical(), ( 800.0, 600.0 ) );
	}

	#[ test ]
	fn viewport_layout_is_physical_and_ignores_dpi_scale()
	{
		// Layout-affecting `Length` values resolve against this, so it must
		// stay in physical space (where the layout tree is computed) even on
		// HiDPI — unlike `viewport_logical`, it does not divide by the scale.
		let mut c = Canvas::new( 720, 1440 );
		assert_eq!( c.viewport_layout(), ( 720.0, 1440.0 ) );
		c.set_dpi_scale( 2.0 );
		assert_eq!( c.viewport_layout(), ( 720.0, 1440.0 ) );
	}

	#[ test ]
	fn geom_px_resolves_widget_design_pixel_per_mode()
	{
		use crate::types::{ set_widget_scaling, set_density, WidgetScaling, Length };
		let _g = crate::TEST_GLOBALS_LOCK.lock().unwrap_or_else( |e| e.into_inner() );

		// Fluid (default): geom_px == fluid( n ) against the physical viewport.
		set_widget_scaling( WidgetScaling::Fluid );
		let c = Canvas::new( 412, 900 );
		assert_eq!( c.geom_px( 48.0 ), Length::fluid( 48.0 ).resolve( ( 412.0, 900.0 ), Length::EM_BASE_DEFAULT ) );

		// Physical: geom_px == n * density, independent of surface size.
		set_widget_scaling( WidgetScaling::Physical );
		set_density( 2.0 );
		assert_eq!( c.geom_px( 48.0 ), 96.0 );

		set_density( 1.0 );
		set_widget_scaling( WidgetScaling::Fluid );
	}

	#[ test ]
	fn canvas_density_overrides_process_density()
	{
		use crate::types::{ set_widget_scaling, set_density, WidgetScaling, Length };
		let _g = crate::TEST_GLOBALS_LOCK.lock().unwrap_or_else( |e| e.into_inner() );

		set_widget_scaling( WidgetScaling::Physical );
		set_density( 1.0 );
		let mut c = Canvas::new( 412, 900 );

		// No local density → the process global applies.
		assert_eq!( c.geom_px( 48.0 ), 48.0 );

		// A pinned canvas density wins over the global, for stock-widget
		// design pixels and explicit dp lengths alike.
		c.set_density( 2.0 );
		assert_eq!( c.density(), 2.0 );
		assert_eq!( c.geom_px( 48.0 ), 96.0 );
		assert_eq!( c.resolve_geom( Length::dp( 10.0 ) ), 20.0 );

		set_widget_scaling( WidgetScaling::Fluid );
	}

	#[ test ]
	fn sub_canvas_inherits_density()
	{
		let _g = crate::TEST_GLOBALS_LOCK.lock().unwrap_or_else( |e| e.into_inner() );

		let mut c = Canvas::new( 412, 900 );
		c.set_density( 3.0 );
		let sub = c.sub_canvas( 100, 100 );
		assert_eq!( sub.density(), 3.0 );
	}

	#[ test ]
	fn text_scale_multiplies_fonts_not_geometry()
	{
		let _g = crate::TEST_GLOBALS_LOCK.lock().unwrap_or_else( |e| e.into_inner() );

		let c = Canvas::new( 400, 400 );
		crate::types::set_text_scale( 1.5 );
		assert_eq!( c.resolve_font( Length::px( 10.0 ) ), 15.0 );
		assert_eq!( c.resolve_geom( Length::px( 10.0 ) ), 10.0 );
		crate::types::set_text_scale( 1.0 );
	}

	#[ test ]
	fn sub_canvas_local_layout_viewport_overrides_inheritance()
	{
		let _g = crate::TEST_GLOBALS_LOCK.lock().unwrap_or_else( |e| e.into_inner() );

		let c = Canvas::new( 1920, 1080 );
		let mut sub = c.sub_canvas( 396, 644 );
		assert_eq!( sub.viewport_layout(), ( 1920.0, 1080.0 ) );
		sub.set_local_layout_viewport();
		assert_eq!( sub.viewport_layout(), ( 396.0, 644.0 ) );
		// Nested sub-canvases keep propagating the pinned local viewport.
		let nested = sub.sub_canvas( 200, 200 );
		assert_eq!( nested.viewport_layout(), ( 396.0, 644.0 ) );
	}

	#[ test ]
	fn font_px_is_constant_physical_in_physical_mode()
	{
		use crate::types::{ set_widget_scaling, set_density, WidgetScaling };
		let _g = crate::TEST_GLOBALS_LOCK.lock().unwrap_or_else( |e| e.into_inner() );

		// Physical mode divides by dpi_scale so `× dpi_scale` at raster yields
		// a constant physical size (n * density).
		set_widget_scaling( WidgetScaling::Physical );
		set_density( 2.0 );
		let mut c = Canvas::new( 720, 1440 );
		c.set_dpi_scale( 2.0 );
		// 16 * 2 (density) / 2 (dpi_scale) = 16 logical → 32 physical after raster.
		assert_eq!( c.font_px( 16.0 ), 16.0 );

		set_density( 1.0 );
		set_widget_scaling( WidgetScaling::Fluid );
	}
}

#[ cfg( test ) ]
mod clip_path_tests
{
	// Exercised on the software backend (`Canvas::new`); the GLES layer-composite
	// path needs a live GL context and is covered by the `clip_path` example.
	use super::Canvas;
	use crate::types::{ Color, PathCmd, Rect };

	fn square( x: f32, y: f32, side: f32 ) -> Vec<PathCmd>
	{
		vec![
			PathCmd::MoveTo( x,        y        ),
			PathCmd::LineTo( x + side, y        ),
			PathCmd::LineTo( x + side, y + side ),
			PathCmd::LineTo( x,        y + side ),
			PathCmd::Close,
		]
	}

	#[ test ]
	fn set_clip_path_bounds_match_the_path_bounding_box()
	{
		let mut c = Canvas::new( 200, 200 );
		c.set_clip_path( &square( 10.0, 20.0, 100.0 ) );
		let b = c.clip_bounds();
		assert_eq!( b.len(), 1 );
		let r = b[ 0 ];
		assert!( ( r.x - 10.0 ).abs() < 0.5 && ( r.y - 20.0 ).abs() < 0.5, "origin {r:?}" );
		assert!( ( r.width - 100.0 ).abs() < 0.5 && ( r.height - 100.0 ).abs() < 0.5, "size {r:?}" );
	}

	#[ test ]
	fn set_clip_path_with_no_segments_clears_the_clip()
	{
		let mut c = Canvas::new( 100, 100 );
		c.set_clip_path( &[] );
		assert!( c.clip_bounds().is_empty() );
	}

	#[ test ]
	fn set_clip_path_masks_a_fill_to_the_path_shape_not_its_bbox()
	{
		let mut c = Canvas::new( 100, 100 );
		// Downward triangle: apex top-centre, base along the bottom.
		let tri = vec![
			PathCmd::MoveTo( 50.0,  5.0 ),
			PathCmd::LineTo( 95.0, 95.0 ),
			PathCmd::LineTo(  5.0, 95.0 ),
			PathCmd::Close,
		];
		c.set_clip_path( &tri );
		c.fill_rect( Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 }, Color::rgba( 1.0, 0.0, 0.0, 1.0 ), 0.0 );

		let Canvas::Software( sc ) = &c else { panic!( "Canvas::new builds a software canvas" ) };
		// A point well inside the triangle is painted; a top corner — inside the
		// bounding box but outside the triangle — stays clear, proving the clip
		// follows the path silhouette rather than its bounding rect.
		let inside = sc.pixmap.pixel( 50, 70 ).expect( "in-bounds pixel" );
		let corner = sc.pixmap.pixel(  8,  8 ).expect( "in-bounds pixel" );
		assert!( inside.red() > 200 && inside.alpha() > 200, "inside triangle must be filled" );
		assert_eq!( corner.alpha(), 0, "bbox corner outside the triangle must stay clear" );
	}
}

#[ cfg( test ) ]
mod pixel_snap_tests
{
	// The GLES backend rounds glyph pen positions and image destinations to
	// integer pixels for crisp 1:1 sampling; these check the software backend
	// now matches that snapping so both backends place content identically.
	use super::Canvas;
	use crate::types::{ Color, Rect };

	fn red_4x4() -> Vec<u8>
	{
		[ 255u8, 0, 0, 255 ].repeat( 16 )
	}

	#[ test ]
	fn image_dest_below_half_snaps_to_the_same_pixels_as_integer_dest()
	{
		let red = red_4x4();
		let mut a = Canvas::new( 32, 32 );
		a.draw_image_data( &red, 4, 4, Rect { x: 5.0, y: 5.0, width: 4.0, height: 4.0 }, 1.0 );
		let mut b = Canvas::new( 32, 32 );
		b.draw_image_data( &red, 4, 4, Rect { x: 5.4, y: 5.4, width: 4.0, height: 4.0 }, 1.0 );

		let Canvas::Software( sa ) = &a else { panic!( "software canvas" ) };
		let Canvas::Software( sb ) = &b else { panic!( "software canvas" ) };
		assert_eq!( sa.pixmap.data(), sb.pixmap.data(), "a sub-half fractional dest snaps to the integer origin" );
		assert!( sa.pixmap.pixel( 5, 5 ).unwrap().red() > 200, "the block lands on pixel (5,5)" );
		assert_eq!( sa.pixmap.pixel( 4, 4 ).unwrap().alpha(), 0, "pixel (4,4) is outside the snapped block" );
	}

	#[ test ]
	fn image_dest_at_or_above_half_rounds_to_the_next_pixel()
	{
		let red = red_4x4();
		let mut c = Canvas::new( 32, 32 );
		c.draw_image_data( &red, 4, 4, Rect { x: 5.6, y: 5.6, width: 4.0, height: 4.0 }, 1.0 );
		let Canvas::Software( sc ) = &c else { panic!( "software canvas" ) };
		assert!( sc.pixmap.pixel( 6, 6 ).unwrap().red() > 200, "round(5.6)=6 moves the block one pixel" );
		assert_eq!( sc.pixmap.pixel( 5, 5 ).unwrap().alpha(), 0, "pixel (5,5) is now clear" );
	}

	#[ test ]
	fn text_pen_position_is_rounded_not_truncated()
	{
		let draw = |x: f32| -> Vec<u8>
		{
			let mut c = Canvas::new( 64, 64 );
			c.draw_text( "l", x, 40.0, 32.0, Color::rgba( 1.0, 1.0, 1.0, 1.0 ) );
			let Canvas::Software( sc ) = &c else { panic!( "software canvas" ) };
			sc.pixmap.data().to_vec()
		};

		let at_int  = draw( 20.0 );
		let at_low  = draw( 20.4 );
		let at_high = draw( 20.6 );

		assert!( at_int.iter().any( |&b| b != 0 ), "the glyph must paint some pixels" );
		assert_eq!( at_int, at_low,  "x and x+0.4 round to the same pixel column" );
		assert_ne!( at_int, at_high, "x+0.6 rounds up to the next column" );
	}
}
