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

//! FBO / framebuffer management for [`GlesCanvas`]: sub-canvas blit,
//! main-FBO ⇄ default-framebuffer present, lazy auxiliary FBO for
//! snapshot-based effects (Overlay blend inset shadow), and the
//! externally-exposed borrowed-texture view.
//!
//! See `primitives.rs` module doc for the canvas-wide `unsafe` contract
//! shared by every block in this file. Per-block notes below only call
//! out what is specific to the operation.

use glow::HasContext;

use crate::types::Rect;

use super::helpers::{ alloc_fbo_tex, native_framebuffer_id, native_texture_id, ortho_rect };
use super::raii::{ FboBinding, ProgramBinding };
use super::{ BorrowedGlesTexture, GlesCanvas };

impl GlesCanvas
{
	/// Composite `src`'s FBO into this canvas at top-left `( dest_x, dest_y )`,
	/// premultiplied over. `src` must share this canvas's GL context (guaranteed
	/// for sub-canvases). Equivalent to [`Self::blit_fade_bottom`] with no fade.
	pub fn blit( &mut self, src: &GlesCanvas, dest_x: i32, dest_y: i32 )
	{
		self.blit_fade_bottom( src, dest_x, dest_y, 0.0 );
	}

	/// Blit `src` into this canvas at `( dest_x, dest_y )`, optionally feathering
	/// the last `fade_bottom_px` source rows so the bottom edge dissolves into
	/// transparency instead of cutting off cleanly. Used by viewports whose
	/// bottom edge is the leading edge of a slide-down animation, where a hard
	/// cut against the underlying layer reads as a knife. With `fade_bottom_px
	/// == 0.0` this matches [`Self::blit`] exactly.
	pub fn blit_fade_bottom( &mut self, src: &GlesCanvas, dest_x: i32, dest_y: i32, fade_bottom_px: f32 )
	{
		self.activate_target();
		let dest = Rect
		{
			x:      dest_x as f32,
			y:      dest_y as f32,
			width:  src.width  as f32,
			height: src.height as f32,
		};
		let mvp        = ortho_rect( self.width, self.height, dest );
		let alpha      = self.global_alpha;
		let height_px  = src.height as f32;
		let fade_clamp = fade_bottom_px.max( 0.0 ).min( height_px );
		// SAFETY: `src.fbo_tex` is owned by `src` (a `&GlesCanvas` argument)
		// and outlives the call. `src` and `self` share the same `Arc<glow::Context>`
		// — verified by construction (sub-canvases are built via `sub_canvas`,
		// which clones `Arc::clone(&self.gl)`) — so sampling `src`'s texture
		// from `self`'s FBO is well-defined.
		unsafe
		{
			// Both the main canvas and the sub-canvas FBO hold premultiplied
			// colour, and the global blend is `(ONE, ONE_MINUS_SRC_ALPHA)` —
			// the premul over-composite formula this blit needs. No temporary
			// blend switch necessary.
			self.gl.use_program( Some( self.sub_blit_program ) );
			self.gl.uniform_matrix_4_f32_slice( Some( &self.u_subblit_mvp ), false, &mvp );
			self.gl.uniform_1_f32( Some( &self.u_subblit_opacity ), alpha );
			self.gl.uniform_1_f32( Some( &self.u_subblit_fade_bottom ), fade_clamp );
			self.gl.uniform_1_f32( Some( &self.u_subblit_height_px ), height_px );
			self.gl.active_texture( glow::TEXTURE0 );
			self.gl.bind_texture( glow::TEXTURE_2D, Some( src.fbo_tex ) );
			self.gl.uniform_1_i32( Some( &self.u_subblit_sampler ), 0 );
			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 );
		}
	}

	/// Re-bind our FBO + viewport + scissor as the active GL state. Cheap and
	/// idempotent, called at the top of every draw / clear / clip method so
	/// that switching between canvases (e.g. main → sub-canvas → main) leaves
	/// each one's state correct without explicit "make active" calls.
	///
	/// Why this exists: GL state (FBO binding, scissor box, viewport) is
	/// global — there is no implicit per-canvas state. When rendering
	/// switches between targets, every method on the active canvas must
	/// reassert its own FBO + viewport, plus re-enable its own scissor (or
	/// disable scissor when the canvas has no clip).
	pub( super ) fn activate_target( &self )
	{
		// While a path clip is active, draws are redirected to the offscreen clip
		// layer (unclipped); the anti-aliased mask is applied when the layer is
		// composited back on clip end. Otherwise draws go to `fbo` with the
		// active scissor.
		// SAFETY: rebinds a canvas-owned FBO + viewport + scissor. All values
		// (`self.fbo`, `self.clip_layer`, `self.width/height`, `self.clip_scissor`)
		// live as long as `&self`, and the bind is idempotent.
		let target = match ( self.clip_layer_active, self.clip_layer )
		{
			( true, Some( ( fbo, _ ) ) ) => fbo,
			_                            => self.fbo,
		};
		let to_layer = self.clip_layer_active && self.clip_layer.is_some();
		unsafe
		{
			self.gl.bind_framebuffer( glow::FRAMEBUFFER, Some( target ) );
			self.gl.viewport( 0, 0, self.width as i32, self.height as i32 );
			match self.clip_scissor
			{
				Some( r ) if !to_layer =>
				{
					let ( x, y, w, h ) = self.scissor_pixels( r );
					self.gl.enable( glow::SCISSOR_TEST );
					self.gl.scissor( x, y, w, h );
				}
				_ =>
				{
					self.gl.disable( glow::SCISSOR_TEST );
				}
			}
		}
	}

	/// Return a borrowed descriptor for the FBO color texture
	/// containing the latest rendered pixels.
	///
	/// `y_inverted` is `true`: the FBO uses GL's native lower-left
	/// origin, so row 0 in texture memory is the bottom of the
	/// rendered image. Consumers that follow the same convention flip
	/// during sampling when this flag is set, producing a correctly-
	/// oriented result. The CPU-side counterpart
	/// [`Self::read_rgba_pixels`] does the same flip inline so the
	/// byte buffer is top-down.
	pub fn borrowed_texture( &self ) -> BorrowedGlesTexture
	{
		BorrowedGlesTexture
		{
			texture_id:     native_texture_id( self.fbo_tex ),
			framebuffer_id: native_framebuffer_id( self.fbo ),
			texture:        self.fbo_tex,
			framebuffer:    self.fbo,
			width:          self.width,
			height:         self.height,
			premultiplied:  true,
			y_inverted:     true,
		}
	}

	/// Read the FBO color attachment into `out` as tightly packed RGBA8,
	/// top-left row first.
	///
	/// This is a compatibility escape hatch. It forces a GPU→CPU sync and
	/// should not be used in steady-state hot paths.
	pub fn read_rgba_pixels( &self, out: &mut [u8] ) -> Result<(), String>
	{
		let needed = self.width as usize * self.height as usize * 4;
		if out.len() < needed
		{
			return Err( format!(
				"read_rgba_pixels needs {needed} bytes, got {}",
				out.len(),
			) );
		}

		let mut raw = vec![ 0_u8; needed ];
		// SAFETY: `raw.len() == needed == width * height * 4` and PACK_ALIGNMENT
		// is set to 1, so `read_pixels` writes exactly `needed` bytes into a
		// buffer of exactly that size. `RGBA + UNSIGNED_BYTE` is the only
		// guaranteed-readable format on every GLES2/3 driver.
		unsafe
		{
			self.gl.bind_framebuffer( glow::FRAMEBUFFER, Some( self.fbo ) );
			self.gl.pixel_store_i32( glow::PACK_ALIGNMENT, 1 );
			self.gl.read_pixels(
				0,
				0,
				self.width as i32,
				self.height as i32,
				glow::RGBA,
				glow::UNSIGNED_BYTE,
				glow::PixelPackData::Slice( Some( &mut raw ) ),
			);
		}
		let stride = self.width as usize * 4;
		for y in 0..self.height as usize
		{
			let src = ( self.height as usize - 1 - y ) * stride;
			let dst = y * stride;
			out[ dst..dst + stride ].copy_from_slice( &raw[ src..src + stride ] );
		}
		Ok( () )
	}

	/// Lazily allocate the auxiliary FBO+texture pair used as a snapshot of
	/// `fbo` for framebuffer-fetch-style effects (Overlay blend,
	/// backdrop-blur source). The pair is sized to match the canvas so
	/// `gl_FragCoord.xy / canvas_size` samples the right texel.
	///
	/// Returns the texture handle of `aux_a`. The FBO is only needed for
	/// the backdrop blur passes that write into `aux_b`; Overlay only
	/// reads from `aux_a`, so this helper keeps the blur-only `aux_b`
	/// allocation deferred until it is actually needed.
	fn ensure_aux_a( &mut self ) -> glow::Texture
	{
		if self.aux_a.is_none()
		{
			// SAFETY: `alloc_fbo_tex` is `unsafe fn`; its requirement (current
			// GL context) holds. Same FBO build / completeness assertion as
			// `setup.rs::new`. We deliberately leave `aux_a`'s FBO as the live
			// binding — the next draw goes through `activate_target` which
			// rebinds `self.fbo`.
			unsafe
			{
				let fbo = self.gl.create_framebuffer().expect( "aux_a FBO" );
				let tex = 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,
				);
				let status = self.gl.check_framebuffer_status( glow::FRAMEBUFFER );
				assert_eq!( status, glow::FRAMEBUFFER_COMPLETE, "aux_a FBO incomplete: 0x{status:x}" );
				self.aux_a = Some( ( fbo, tex ) );
			}
		}
		self.aux_a.expect( "just allocated" ).1
	}

	/// Snapshot variant that additionally clamps `region` to the active
	/// scissor. Safe only for shaders that sample the snapshot at
	/// exactly one point per fragment.
	pub( super ) fn snapshot_fbo_region_tight( &mut self, region: Rect )
	{
		self.snapshot_fbo_region_impl( region, true )
	}

	fn snapshot_fbo_region_impl( &mut self, region: Rect, intersect_scissor: bool )
	{
		let aux_tex = self.ensure_aux_a();
		// Clamp `region` to canvas bounds. `copy_tex_sub_image_2d` would
		// generate `INVALID_VALUE` (or undefined behaviour on some
		// drivers) if the source rect extends outside the framebuffer.
		let cw = self.width  as f32;
		let ch = self.height as f32;
		let mut x0 = region.x.max( 0.0 );
		let mut y0_top = region.y.max( 0.0 );
		let mut x1 = ( region.x + region.width  ).min( cw );
		let mut y1_top = ( region.y + region.height ).min( ch );
		if intersect_scissor
		{
			if let Some( clip ) = self.clip_scissor
			{
				x0     = x0.max( clip.x );
				y0_top = y0_top.max( clip.y );
				x1     = x1.min( clip.x + clip.width );
				y1_top = y1_top.min( clip.y + clip.height );
			}
		}
		let w = ( x1 - x0 ).floor() as i32;
		let h = ( y1_top - y0_top ).floor() as i32;
		if w <= 0 || h <= 0 { return; }
		// GL framebuffer origin is bottom-left; our rect is top-left.
		let src_x = x0.floor() as i32;
		let src_y = self.height as i32 - y0_top.floor() as i32 - h;
		// SAFETY: the four bounds checks above guarantee `(src_x, src_y, w, h)`
		// lies fully inside `self.fbo`'s colour attachment, so
		// `copy_tex_sub_image_2d` will not raise INVALID_VALUE. `aux_tex` was
		// allocated through `ensure_aux_a` to canvas dimensions, so the
		// destination region is also in-bounds.
		unsafe
		{
			self.gl.bind_framebuffer( glow::FRAMEBUFFER, Some( self.fbo ) );
			self.gl.bind_texture( glow::TEXTURE_2D, Some( aux_tex ) );
			self.gl.copy_tex_sub_image_2d
			(
				glow::TEXTURE_2D, 0,
				src_x, src_y,
				src_x, src_y,
				w, h,
			);
			self.gl.bind_texture( glow::TEXTURE_2D, None );
		}
	}

	/// Drop both auxiliary FBO+texture pairs if allocated. Called from
	/// [`Self::resize`] so the next effect re-allocates at the new size.
	pub( super ) fn invalidate_aux( &mut self )
	{
		// SAFETY: each (fbo, tex) pair was created through `self.gl` in
		// `ensure_aux_a` / `ensure_aux_b`, so deleting through the same
		// context is well-defined. `take()` ensures we never double-delete.
		unsafe
		{
			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 );
			}
		}
	}

	/// Blit the FBO color attachment onto the default framebuffer (the EGL
	/// window). Caller is responsible for the `eglSwapBuffers` that
	/// publishes the result. After present, the FBO is rebound so the next
	/// frame's draws keep accumulating into the shadow canvas.
	///
	/// The blit always covers the full surface — partial-redraw still saves
	/// work upstream (only changed widget pixels are repainted into the
	/// FBO), but the FBO→FB0 transfer itself is a single cheap fullscreen
	/// op.
	pub fn present( &mut self )
	{
		// Scoped guards: bind the default framebuffer (id 0) and the
		// blit program for the duration of this fn. On Drop they restore
		// `self.fbo` and "no program", so any future early-return / panic
		// in the blit body cannot leave the canvas pointing at the wrong
		// FBO or program. The viewport, blend and scissor are restored
		// inline below — they are non-resource state that does not need
		// the guard treatment because `activate_target` rewrites them on
		// every subsequent draw.
		//
		// SAFETY: GL context is current (canvas invariant). `self.fbo` is
		// the canvas-owned FBO from `setup.rs::new`. `self.blit_program`
		// was linked in `setup.rs::new`. Restoring "no program active"
		// (`None`) is always sound.
		let _fbo  = unsafe { FboBinding::scoped( &self.gl, None, Some( self.fbo ) ) };
		let _prog = unsafe { ProgramBinding::scoped( &self.gl, Some( self.blit_program ), None ) };
		// SAFETY: see above. The block sets up the blit pipeline state,
		// draws a fullscreen quad sampling `fbo_tex`, then restores blend
		// and viewport so the next frame's draws inherit the canvas-wide
		// defaults. FBO + program are restored by the guards on scope exit.
		unsafe
		{
			self.gl.viewport( 0, 0, self.width as i32, self.height as i32 );
			self.gl.disable( glow::SCISSOR_TEST );
			self.gl.disable( glow::BLEND );
			self.gl.active_texture( glow::TEXTURE0 );
			self.gl.bind_texture( glow::TEXTURE_2D, Some( self.fbo_tex ) );
			self.gl.uniform_1_i32( Some( &self.u_blit_sampler ), 0 );
			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.enable( glow::BLEND );
			self.gl.viewport( 0, 0, self.width as i32, self.height as i32 );
		}
		// Scissor was disabled above; reflect that in our cached state.
		self.clip_scissor = None;
		// Guards drop here: FBO → self.fbo, program → None.
	}
}
