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

//! Clipping + whole-canvas fill / clear for [`GlesCanvas`]. Rect clips
//! ([`GlesCanvas::set_clip_rects`]) use `glScissor`: when given multiple rects
//! the bounding-box union is the scissor — coarse, but the partial-redraw path
//! normally clusters 1–3 rects so the union is barely larger than the sum
//! (disjoint regions are not clipped tightly). Arbitrary path clips
//! ([`GlesCanvas::set_clip_path`]) are anti-aliased: the clipped draws are
//! captured into an offscreen layer FBO, then composited back onto the canvas
//! multiplied by a CPU-rasterised (tiny-skia) anti-aliased coverage mask, so the
//! clipped edge is smooth rather than the hard edge a 1-bit stencil would give.

use glow::HasContext;

use crate::types::{ Color, Rect };

use super::GlesCanvas;

impl GlesCanvas
{
	/// Clip subsequent draws to `rects` via `glScissor`. The scissor is the
	/// bounding-box union of all rects clamped to the canvas — a coarse clip,
	/// unlike the software backend's exact per-rect mask, so pixels between
	/// disjoint rects are not culled. An empty slice clears the clip; an empty
	/// union installs a zero-area scissor so subsequent draws become no-ops.
	/// Replaces any active path clip, flushing its layer first.
	pub fn set_clip_rects( &mut self, rects: &[Rect] )
	{
		// A rect clip replaces any active path clip: flush its layer first.
		self.flush_clip_layer();
		// Scissor is global GL state; rebind our FBO first so the scissor
		// applies to this canvas and not whatever target was active before.
		self.activate_target();
		if rects.is_empty()
		{
			self.clear_clip();
			return;
		}
		let mut x0 =  f32::INFINITY;
		let mut y0 =  f32::INFINITY;
		let mut x1 = -f32::INFINITY;
		let mut y1 = -f32::INFINITY;
		for r in rects
		{
			x0 = x0.min( r.x );
			y0 = y0.min( r.y );
			x1 = x1.max( r.x + r.width  );
			y1 = y1.max( r.y + r.height );
		}
		x0 = x0.max( 0.0 );
		y0 = y0.max( 0.0 );
		x1 = x1.min( self.width  as f32 );
		y1 = y1.min( self.height as f32 );
		if x1 <= x0 || y1 <= y0
		{
			// Empty union — install a zero-area scissor so subsequent draws
			// are no-ops without disabling the test.
			self.set_scissor( Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 } );
			return;
		}
		self.set_scissor( Rect { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } );
	}

	/// Drop the active clip — disable the scissor test and flush any open path
	/// clip layer (compositing it back). Subsequent draws cover the whole canvas.
	pub fn clear_clip( &mut self )
	{
		// SAFETY: see `primitives.rs` module doc. `disable( SCISSOR_TEST )` is
		// pure global-state mutation; the cached `clip_scissor` is updated
		// to match below.
		unsafe { self.gl.disable( glow::SCISSOR_TEST ); }
		self.clip_scissor = None;
		self.flush_clip_layer();
	}

	/// Allocate the offscreen clip layer (a full-canvas colour FBO) on first
	/// use. Freed and re-allocated at the new size by `resize`.
	fn ensure_clip_layer( &mut self )
	{
		if self.clip_layer.is_some() { return; }
		// SAFETY: GL context current (canvas invariant). Allocates an FBO + colour
		// texture sized to the canvas and attaches it; the handles are stored so
		// `Drop`/`resize` manage their lifetime. Restores the canvas FBO binding.
		unsafe
		{
			let fbo = self.gl.create_framebuffer().expect( "clip-layer FBO" );
			let tex = super::helpers::alloc_fbo_tex( &self.gl, self.version, self.width, self.height );
			self.gl.bind_framebuffer( glow::FRAMEBUFFER, Some( fbo ) );
			self.gl.framebuffer_texture_2d( glow::FRAMEBUFFER, glow::COLOR_ATTACHMENT0, glow::TEXTURE_2D, Some( tex ), 0 );
			self.gl.bind_framebuffer( glow::FRAMEBUFFER, Some( self.fbo ) );
			self.clip_layer = Some( ( fbo, tex ) );
		}
	}

	/// Clip subsequent draws to an arbitrary vector path, with an anti-aliased
	/// edge. Begins capturing draws into the offscreen clip layer; the path's
	/// anti-aliased coverage is rasterised (tiny-skia) into `clip_mask_tex` and
	/// applied when the layer is composited back by `flush_clip_layer` (on the
	/// next `clear_clip` / `set_clip_rects`). GLES counterpart of the software
	/// backend's `set_clip_path`.
	pub fn set_clip_path( &mut self, cmds: &[ crate::types::PathCmd ] )
	{
		// A new path clip supersedes any layer still open.
		self.flush_clip_layer();
		let Some( path ) = crate::render::helpers::build_ts_path( cmds ) else { self.clear_clip(); return; };
		let b  = path.bounds();
		let x0 = b.left().floor().max( 0.0 );
		let y0 = b.top().floor().max( 0.0 );
		let x1 = b.right().ceil().min( self.width  as f32 );
		let y1 = b.bottom().ceil().min( self.height as f32 );
		let pw = ( x1 - x0 ).max( 0.0 ) as u32;
		let ph = ( y1 - y0 ).max( 0.0 ) as u32;
		if pw == 0 || ph == 0 || pw > 8192 || ph > 8192 { self.clear_clip(); return; }

		// Rasterise the path coverage (anti-aliased, opaque inside) into a
		// bbox-sized pixmap; the composite shader multiplies the layer by it.
		let Some( mut pixmap ) = tiny_skia::Pixmap::new( pw, ph ) else { return; };
		let mut paint = tiny_skia::Paint::default();
		paint.set_color( tiny_skia::Color::WHITE );
		paint.anti_alias = true;
		let tf = tiny_skia::Transform::from_translate( -x0, -y0 );
		pixmap.fill_path( &path, &paint, tiny_skia::FillRule::Winding, tf, None );
		let rgba = pixmap.data().to_vec();

		let mask = super::helpers::upload_rgba_texture( &self.gl, self.version, &rgba, pw as i32, ph as i32 );
		self.clip_mask_tex = Some( mask );
		self.clip_bbox     = Rect { x: x0, y: y0, width: pw as f32, height: ph as f32 };

		self.ensure_clip_layer();
		// Redirect to the layer and clear it transparent so untouched pixels stay
		// empty (they contribute nothing to the composite).
		self.clip_layer_active = true;
		self.clip_scissor      = None;
		self.activate_target();
		// SAFETY: GL context current; `activate_target` bound the layer FBO.
		unsafe
		{
			self.gl.clear_color( 0.0, 0.0, 0.0, 0.0 );
			self.gl.clear( glow::COLOR_BUFFER_BIT );
		}
	}

	/// Composite the clip layer back onto the canvas FBO, multiplied by the
	/// anti-aliased coverage mask, then end the path clip. No-op when no path
	/// clip is open.
	fn flush_clip_layer( &mut self )
	{
		if !self.clip_layer_active { return; }
		self.clip_layer_active = false;
		let Some( ( _, layer_tex ) ) = self.clip_layer else { return; };
		let Some( mask_tex ) = self.clip_mask_tex.take() else { return; };
		let bbox = self.clip_bbox;
		let mvp  = super::helpers::ortho_rect( self.width, self.height, bbox );
		// SAFETY: GL context current. Bind the canvas FBO, disable scissor, and
		// draw a quad over the bbox sampling the layer (unit 0) and coverage mask
		// (unit 1); the premultiplied result is blended SRC_OVER. All handles are
		// canvas-owned; the transient mask texture is deleted after the draw.
		unsafe
		{
			self.gl.bind_framebuffer( glow::FRAMEBUFFER, Some( self.fbo ) );
			self.gl.viewport( 0, 0, self.width as i32, self.height as i32 );
			self.gl.disable( glow::SCISSOR_TEST );
			self.gl.use_program( Some( self.clip_composite_program ) );
			self.gl.uniform_matrix_4_f32_slice( Some( &self.u_clip_mvp ), false, &mvp );
			self.gl.uniform_2_f32( Some( &self.u_clip_canvas ), self.width as f32, self.height as f32 );
			self.gl.uniform_4_f32( Some( &self.u_clip_bbox ), bbox.x, bbox.y, bbox.width, bbox.height );
			self.gl.active_texture( glow::TEXTURE0 );
			self.gl.bind_texture( glow::TEXTURE_2D, Some( layer_tex ) );
			self.gl.uniform_1_i32( Some( &self.u_clip_layer ), 0 );
			self.gl.active_texture( glow::TEXTURE1 );
			self.gl.bind_texture( glow::TEXTURE_2D, Some( mask_tex ) );
			self.gl.uniform_1_i32( Some( &self.u_clip_mask ), 1 );
			self.gl.bind_vertex_array( Some( self.quad_vao ) );
			self.gl.draw_arrays( glow::TRIANGLES, 0, 6 );
			self.gl.bind_vertex_array( None );
			self.gl.bind_texture( glow::TEXTURE_2D, None );
			self.gl.active_texture( glow::TEXTURE0 );
			self.gl.bind_texture( glow::TEXTURE_2D, None );
			self.gl.delete_texture( mask_tex );
		}
	}

	/// Snapshot of the active scissor as a `Vec<Rect>` (empty when no
	/// scissor is set).
	pub fn clip_bounds_snapshot( &self ) -> Vec<Rect>
	{
		self.clip_scissor.map_or_else( Vec::new, |r| vec![ r ] )
	}

	/// Apply `rect` as the current scissor (top-left coords, GL bottom-left).
	fn set_scissor( &mut self, rect: Rect )
	{
		let ( x, y, w, h ) = self.scissor_pixels( rect );
		// SAFETY: `scissor_pixels` clamps to non-negative integers; GL accepts
		// arbitrary scissor rects (regions outside the framebuffer simply
		// cull all fragments). State change is mirrored in `clip_scissor`.
		unsafe
		{
			self.gl.enable( glow::SCISSOR_TEST );
			self.gl.scissor( x, y, w, h );
		}
		self.clip_scissor = Some( rect );
	}

	/// Convert a top-left rect to the bottom-left integer pixel scissor that
	/// GL expects.
	pub( super ) fn scissor_pixels( &self, rect: Rect ) -> ( i32, i32, i32, i32 )
	{
		let x = rect.x.floor() as i32;
		let w = rect.width.ceil()  as i32;
		let h = rect.height.ceil() as i32;
		// GL origin is bottom-left, our coords are top-left.
		let y_top    = rect.y.floor() as i32;
		let y_bottom = self.height as i32 - y_top - h;
		( x, y_bottom.max( 0 ), w.max( 0 ), h.max( 0 ) )
	}

	/// Clear to a solid color. Honours the active scissor — if a clip is set,
	/// only the clipped region is filled.
	pub fn fill( &mut self, color: Color )
	{
		self.activate_target();
		// SAFETY: `clear` writes the configured `clear_color` into every
		// fragment that survives the scissor test (which `activate_target`
		// has already configured to match `clip_scissor`).
		unsafe
		{
			self.gl.clear_color( color.r, color.g, color.b, color.a );
			self.gl.clear( glow::COLOR_BUFFER_BIT );
		}
	}

	/// Clear to fully transparent. Honours the active scissor.
	pub fn clear( &mut self )
	{
		self.activate_target();
		// SAFETY: same as `fill`, with a transparent clear colour.
		unsafe
		{
			self.gl.clear_color( 0.0, 0.0, 0.0, 0.0 );
			self.gl.clear( glow::COLOR_BUFFER_BIT );
		}
	}

	/// Zero the pixels inside each rect (alpha+RGB → 0).
	pub fn clear_rects_transparent( &mut self, rects: &[Rect] )
	{
		self.activate_target();
		let saved = self.clip_scissor;
		// SAFETY: enable scissor + set transparent clear colour once for
		// the whole loop; per-rect we rewrite `scissor` and clear. After
		// the loop we restore `saved` via `set_scissor` / `clear_clip`
		// so the cached `clip_scissor` matches the actual GL state again.
		unsafe
		{
			self.gl.enable( glow::SCISSOR_TEST );
			self.gl.clear_color( 0.0, 0.0, 0.0, 0.0 );
		}
		for r in rects
		{
			let ( x, y, w, h ) = self.scissor_pixels( *r );
			if w <= 0 || h <= 0 { continue; }
			// SAFETY: per-rect scissor + clear; same invariants as above.
			unsafe
			{
				self.gl.scissor( x, y, w, h );
				self.gl.clear( glow::COLOR_BUFFER_BIT );
			}
		}
		// Restore the previous scissor (or disable if none was active).
		match saved
		{
			Some( r ) => self.set_scissor( r ),
			None      => self.clear_clip(),
		}
	}

}
