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

//! RAII guards for scoped GL bindings.
//!
//! The renderer normally restores GL state lazily: every draw method
//! calls [`super::GlesCanvas::activate_target`] at entry, which
//! re-binds the canvas FBO and reasserts viewport / scissor. So a
//! method that temporarily binds a different FBO (e.g. `aux_a` for a
//! snapshot) does not need to restore the previous binding — the next
//! draw will. That is a deliberate optimisation; introducing a RAII
//! restore at every site would double-bind the canvas FBO on every
//! frame.
//!
//! These guards exist for the *opposite* shape — operations that
//! genuinely change global state for the duration of a scope and
//! must guarantee restoration **even on early return / panic**.
//! Today that fits exactly one site: [`super::GlesCanvas::present`],
//! which binds the default framebuffer (id 0) and the blit program,
//! draws a fullscreen quad, and rebinds the canvas FBO + previous
//! program. Without RAII, an early return between the two binds
//! would leave the GL context with the default FBO and the wrong
//! program active — the next draw on this canvas would render to
//! the wrong target until `activate_target` ran (FBO is corrected
//! by `activate_target`; `use_program` is **not**).
//!
//! When in doubt: do not wrap `bind_framebuffer` / `use_program` in
//! a guard "for safety". The lazy-restore convention is part of the
//! renderer's contract and the guard pays for itself only when the
//! scope can exit through a path that bypasses `activate_target`.

use glow::HasContext;

/// Scoped framebuffer binding. Binds `target` on construction and
/// restores `previous` on Drop. Caller passes `previous` explicitly
/// because every site that needs this guard already knows what it
/// wants to restore (typically `self.fbo`) — querying
/// `GL_FRAMEBUFFER_BINDING` would force a sync round-trip we do not
/// need.
pub( super ) struct FboBinding<'a>
{
	gl:       &'a glow::Context,
	previous: Option<glow::Framebuffer>,
}

impl<'a> FboBinding<'a>
{
	/// Bind `target` immediately; remember `previous` to rebind on Drop.
	///
	/// # Safety
	///
	/// The GL context behind `gl` must be current on the calling thread
	/// for the entire lifetime of the returned guard. Both `target` and
	/// `previous` must be names produced by this same context (or
	/// `None` for the default framebuffer).
	pub( super ) unsafe fn scoped( gl: &'a glow::Context, target: Option<glow::Framebuffer>, previous: Option<glow::Framebuffer> ) -> Self
	{
		// SAFETY: forwarded from the fn's own `# Safety` contract.
		unsafe { gl.bind_framebuffer( glow::FRAMEBUFFER, target ); }
		Self { gl, previous }
	}
}

impl<'a> Drop for FboBinding<'a>
{
	fn drop( &mut self )
	{
		// SAFETY: the construction-time invariant (current context) is the
		// guard's lifetime invariant. Restoring `previous` is a pure
		// state-machine mutation.
		unsafe { self.gl.bind_framebuffer( glow::FRAMEBUFFER, self.previous ); }
	}
}

/// Scoped program binding. Same shape as [`FboBinding`] but for
/// `glUseProgram`. Caller passes the program to restore explicitly
/// (typically the program the next pipeline stage will need, or
/// `None` to leave nothing bound).
pub( super ) struct ProgramBinding<'a>
{
	gl:       &'a glow::Context,
	previous: Option<glow::Program>,
}

impl<'a> ProgramBinding<'a>
{
	/// Activate `target` immediately; remember `previous` to restore on Drop.
	///
	/// # Safety
	///
	/// Same as [`FboBinding::scoped`].
	pub( super ) unsafe fn scoped( gl: &'a glow::Context, target: Option<glow::Program>, previous: Option<glow::Program> ) -> Self
	{
		// SAFETY: forwarded from the fn's own `# Safety` contract.
		unsafe { gl.use_program( target ); }
		Self { gl, previous }
	}
}

impl<'a> Drop for ProgramBinding<'a>
{
	fn drop( &mut self )
	{
		// SAFETY: see `FboBinding::drop`.
		unsafe { self.gl.use_program( self.previous ); }
	}
}
