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

//! EGL bootstrap for the GPU rendering path.
//!
//! Initialises an `EGLDisplay` from the Wayland connection, picks an
//! `EGLConfig`, and creates an `EGLContext`. Tries GLES 3 first and falls
//! back to GLES 2 if the driver does not advertise it. On success returns an
//! [`EglContext`] holding a `glow::Context` already pointed at the resolved
//! GL functions; on failure returns `Err( reason )` so the caller can fall
//! back to the software `wl_shm` path.
//!
//! Per-surface, [`EglSurface`] wraps a `wl_egl_window` plus an `EGLSurface`
//! pinned to the wayland surface. Resizing the wayland surface must call
//! [`EglSurface::resize`] so the underlying buffer follows.
//!
//! The bootstrap honours `LTK_FORCE_SOFTWARE=1` by failing fast with a
//! descriptive error. Backend selection is announced exactly once per
//! process via `eprintln!( "[ltk] render backend: {GLES3|GLES2|SOFTWARE (...)}" )`
//! — the GPU branch logs from [`EglContext::new`], the SOFTWARE branch from
//! [`log_software_fallback`] (called by the integration site in `draw.rs`).

use khronos_egl as egl;
use std::ffi::c_void;
use std::sync::{ Arc, Once, OnceLock };

use crate::gles_render::GlesVersion;
use smithay_client_toolkit::reexports::client::
{
	Connection,
	Proxy,
	protocol::wl_surface::WlSurface,
};

type EglInstance = egl::DynamicInstance<egl::EGL1_4>;

/// `eglSwapBuffersWithDamageKHR` / `EXT` signature. Loaded at runtime via
/// `eglGetProcAddress` when the corresponding extension is advertised.
///
/// Why we need this: Mesa's plain `eglSwapBuffers` emits
/// `wl_surface.damage(0, 0, INT32_MAX, INT32_MAX)` as a "damage everything"
/// sentinel. Some compositor renderers (sway/wlroots GLES backend at the
/// time of writing) consume that value literally, calling
/// `glTexSubImage2D(0, 0, INT32_MAX, INT32_MAX)` to upload the wl_buffer to
/// their internal cache texture. That fails with `GL_INVALID_VALUE` and the
/// compositor keeps showing the previously-cached frame (the first frame
/// goes through `glTexImage2D` which doesn't have this bug, so the *initial*
/// render is fine — but every subsequent eglSwapBuffers is silently dropped).
///
/// Using `eglSwapBuffersWithDamage` makes Mesa emit
/// `wl_surface.damage_buffer(x, y, w, h)` with the actual rect we pass,
/// which the compositor then uploads correctly.
type SwapBuffersWithDamageFn = unsafe extern "system" fn(
	display: egl::EGLDisplay,
	surface: egl::EGLSurface,
	rects:   *const egl::Int,
	n_rects: egl::Int,
) -> egl::Boolean;

/// Process-wide EGL display + GLES context. Cheap to clone because the heavy
/// state (`Arc<EglInstance>`, `Arc<glow::Context>`) is reference-counted; the
/// raw EGL handles are POD.
pub struct EglContext
{
	/// Refcounted handle to the loaded `libEGL` instance. Shared with every
	/// [`EglSurface`] this context creates.
	pub egl:     Arc<EglInstance>,
	/// The initialised `EGLDisplay` derived from the Wayland connection.
	pub display: egl::Display,
	/// The chosen `EGLConfig` (8-8-8-8 RGBA, window-renderable, GLES2-capable);
	/// every surface and the context are created against it.
	pub config:  egl::Config,
	/// The `EGLContext` made current by [`Self::make_current`].
	pub context: egl::Context,
	/// The GLES major version actually obtained (ES3 preferred, ES2 fallback).
	pub version: GlesVersion,
	// Initialised lazily on the first `make_current`. Glow's constructor
	// eagerly calls `glGetString( GL_VERSION )`, which requires a current
	// context — and at `EglContext::new` time there is no surface yet.
	gl:          OnceLock<Arc<glow::Context>>,
	/// Resolved `eglSwapBuffersWithDamageKHR` / `EXT` pointer when either
	/// extension is advertised, `None` otherwise. The draw path uses it in
	/// place of `eglSwapBuffers` to avoid Mesa's `INT32_MAX` damage sentinel
	/// (see [`SwapBuffersWithDamageFn`] docs).
	swap_with_damage: Option<SwapBuffersWithDamageFn>,
}

/// Per-Wayland-surface EGL window. `egl_window` owns the `wl_egl_window`; it
/// must outlive `surface` because EGL keeps a raw pointer into it. `egl` and
/// `display` are kept so that `Drop` can call `eglDestroySurface` without
/// requiring a `&EglContext` at the call site.
pub struct EglSurface
{
	/// The `wl_egl_window` backing this surface. EGL holds a raw pointer into
	/// it, so it must not be dropped while `surface` is alive.
	pub egl_window: wayland_egl::WlEglSurface,
	/// The `EGLSurface` bound to the Wayland surface, current target for GL
	/// rendering once [`EglContext::make_current`] selects it.
	pub surface:    egl::Surface,
	egl:            Arc<EglInstance>,
	display:        egl::Display,
}

/// Runtime-free EGL target for code that wants a GPU [`crate::render::Canvas`]
/// without going through `ltk::run`.
///
/// This owns an EGL display, GLES context, and tiny pbuffer surface. The ltk
/// GLES canvas still renders into its own FBO; the pbuffer exists only to make
/// a valid EGL context current for GL calls. Compositors that already own a GL
/// context should normally create `Canvas::new_gles(...)` themselves and pass it
/// to `UiSurface::from_canvas` instead.
pub struct EglOffscreenContext
{
	egl:     Arc<EglInstance>,
	display: egl::Display,
	config:  egl::Config,
	context: egl::Context,
	surface: egl::Surface,
	version: GlesVersion,
	gl:      Arc<glow::Context>,
}

impl Drop for EglSurface
{
	fn drop( &mut self )
	{
		// Best-effort: errors here can't be acted on, the surface is going away.
		let _ = self.egl.destroy_surface( self.display, self.surface );
	}
}

impl Drop for EglOffscreenContext
{
	fn drop( &mut self )
	{
		let _ = self.egl.make_current( self.display, None, None, None );
		let _ = self.egl.destroy_surface( self.display, self.surface );
		let _ = self.egl.destroy_context( self.display, self.context );
		let _ = self.egl.terminate( self.display );
	}
}

impl EglContext
{
	/// Initialise EGL on `conn`. Returns `Err( reason )` if EGL cannot be
	/// used (forced software, library missing, no compatible config, ES2/3
	/// context creation failed). The caller falls back to SHM and is
	/// expected to log via [`log_software_fallback`].
	pub fn new( conn: &Connection ) -> Result<Self, String>
	{
		if std::env::var( "LTK_FORCE_SOFTWARE" ).map( |v| v != "0" ).unwrap_or( false )
		{
			return Err( "LTK_FORCE_SOFTWARE=1".to_string() );
		}

		// SAFETY: `EglInstance::load_required` performs a `dlopen` on the
		// system `libEGL.so` and is unsafe because the loaded library has
		// arbitrary side effects on global process state. We tolerate that:
		// libEGL is an established system component and ltk has no
		// alternative path to GPU rendering.
		let egl: Arc<EglInstance> = Arc::new(
			unsafe { EglInstance::load_required() }
				.map_err( |e| format!( "load libEGL: {e:?}" ) )?,
		);

		let wl_display_ptr = conn.backend().display_ptr() as *mut c_void;
		// SAFETY: `wl_display_ptr` comes from `Connection::backend().display_ptr()`,
		// which guarantees a valid `wl_display *` for as long as `conn` lives.
		// `conn` is a `&Connection` argument so the pointer is valid for the
		// duration of this call. EGL retains the display reference internally
		// and we keep `egl` alive for the process lifetime.
		let display = unsafe { egl.get_display( wl_display_ptr ) }
			.ok_or_else( || "eglGetDisplay returned NULL".to_string() )?;

		egl.initialize( display )
			.map_err( |e| format!( "eglInitialize: {e:?}" ) )?;

		// Multiple client APIs can coexist on EGL 1.4; we want OpenGL ES.
		egl.bind_api( egl::OPENGL_ES_API )
			.map_err( |e| format!( "eglBindAPI: {e:?}" ) )?;

		let config_attribs = [
			egl::SURFACE_TYPE,    egl::WINDOW_BIT,
			egl::RED_SIZE,        8,
			egl::GREEN_SIZE,      8,
			egl::BLUE_SIZE,       8,
			egl::ALPHA_SIZE,      8,
			egl::RENDERABLE_TYPE, egl::OPENGL_ES2_BIT,
			egl::NONE,
		];
		let config = egl.choose_first_config( display, &config_attribs )
			.map_err( |e| format!( "eglChooseConfig: {e:?}" ) )?
			.ok_or_else( || "no compatible EGL config".to_string() )?;

		// Try ES3 first, fall back to ES2. CONTEXT_CLIENT_VERSION applies to
		// both ES2 and ES3 contexts when the value is the major version.
		let ( context, version ) = match try_create_context( &egl, display, config, 3 )
		{
			Ok( ctx ) => ( ctx, GlesVersion::V3 ),
			Err( _ )  => match try_create_context( &egl, display, config, 2 )
			{
				Ok( ctx ) => ( ctx, GlesVersion::V2 ),
				Err( e )  => return Err( format!( "eglCreateContext (ES2/ES3): {e:?}" ) ),
			},
		};

		log_backend_once( match version
		{
			GlesVersion::V3 => "GLES3",
			GlesVersion::V2 => "GLES2",
		} );

		// Try to resolve eglSwapBuffersWithDamage{KHR,EXT}. The KHR variant is
		// preferred (newer, identical signature). Falling back to plain
		// eglSwapBuffers when neither is available means we keep the latent
		// INT32_MAX-damage bug, but at least we don't silently fail to start.
		let extensions = egl.query_string( Some( display ), egl::EXTENSIONS )
			.ok()
			.and_then( |s| s.to_str().ok() )
			.unwrap_or( "" );
		let proc_name = if extensions.contains( "EGL_KHR_swap_buffers_with_damage" )
		{
			Some( "eglSwapBuffersWithDamageKHR" )
		} else if extensions.contains( "EGL_EXT_swap_buffers_with_damage" ) {
			Some( "eglSwapBuffersWithDamageEXT" )
		} else {
			None
		};
		// SAFETY: `proc_name` is one of the literals
		// `"eglSwapBuffersWithDamageKHR"` / `"eglSwapBuffersWithDamageEXT"`,
		// gated on the matching extension being advertised by `eglQueryString`.
		// When EGL returns a non-null pointer for that name, the symbol is
		// guaranteed by the EGL extension spec to have the
		// `SwapBuffersWithDamageFn` signature. The pointer is stored as
		// `Option<fn>` and only invoked through that typed slot.
		let swap_with_damage: Option<SwapBuffersWithDamageFn> = proc_name
			.and_then( |n| egl.get_proc_address( n ) )
			.map( |p| unsafe { std::mem::transmute::<_, SwapBuffersWithDamageFn>( p ) } );

		Ok( Self { egl, display, config, context, version, gl: OnceLock::new(), swap_with_damage } )
	}

	/// Access the lazily-constructed `glow::Context`. Must be called only after
	/// the first successful `make_current`; panics otherwise.
	pub fn gl( &self ) -> &Arc<glow::Context>
	{
		self.gl.get().expect( "EglContext::gl() called before make_current" )
	}

	/// Create an `EGLSurface` pinned to `wl_surface` at the given pixel size.
	pub fn create_surface(
		&self, wl_surface: &WlSurface, width: i32, height: i32,
	) -> Result<EglSurface, String>
	{
		let id = wl_surface.id();
		let egl_window = wayland_egl::WlEglSurface::new( id, width.max( 1 ), height.max( 1 ) )
			.map_err( |e| format!( "wl_egl_window::new: {e:?}" ) )?;
		// SAFETY: `egl_window` is a freshly-built `WlEglSurface` whose `ptr()`
		// returns the live `wl_egl_window *`. The returned `EglSurface`
		// embeds `egl_window` so the pointer outlives the EGL surface (EGL
		// retains its own internal reference). `display` and `config` are
		// the values we used at context creation, valid for the lifetime
		// of `self`.
		let surface = unsafe
		{
			self.egl.create_window_surface(
				self.display,
				self.config,
				egl_window.ptr() as egl::NativeWindowType,
				None,
			)
		}.map_err( |e| format!( "eglCreateWindowSurface: {e:?}" ) )?;
		Ok( EglSurface
		{
			egl_window,
			surface,
			egl:     Arc::clone( &self.egl ),
			display: self.display,
		} )
	}

	/// Make `surface` current for subsequent GL calls. The first successful
	/// call lazily constructs the shared `glow::Context` (glow needs a live
	/// current context to read `GL_VERSION` during initialisation).
	pub fn make_current( &self, surface: &EglSurface ) -> Result<(), String>
	{
		self.egl.make_current(
			self.display,
			Some( surface.surface ),
			Some( surface.surface ),
			Some( self.context ),
		).map_err( |e| format!( "eglMakeCurrent: {e:?}" ) )?;

		if self.gl.get().is_none()
		{
			let egl_for_loader = Arc::clone( &self.egl );
			// SAFETY: `from_loader_function` is unsafe because it calls
			// `glGetString( GL_VERSION )` during construction, which requires
			// a current context — established by the `make_current` call
			// immediately above. The closure resolves symbols through the
			// retained `egl_for_loader` (refcounted clone) so the loader
			// stays valid for the lifetime of the returned `glow::Context`.
			let gl = Arc::new( unsafe
			{
				glow::Context::from_loader_function( move |name|
				{
					egl_for_loader.get_proc_address( name )
						.map( |p| p as *const _ )
						.unwrap_or( std::ptr::null() )
				} )
			} );
			let _ = self.gl.set( gl );
		}
		Ok( () )
	}

	/// Post the rendered back buffer of `surface` to the compositor via
	/// `eglSwapBuffers`. Requires `surface` to be the current draw surface
	/// (call [`Self::make_current`] first). Damages the whole surface; on Mesa
	/// this emits the `INT32_MAX` damage sentinel, so prefer
	/// [`Self::swap_buffers_with_damage`] where the extension is available.
	pub fn swap_buffers( &self, surface: &EglSurface ) -> Result<(), String>
	{
		self.egl.swap_buffers( self.display, surface.surface )
			.map_err( |e| format!( "eglSwapBuffers: {e:?}" ) )
	}

	/// Like [`Self::swap_buffers`] but submits explicit damage rects so Mesa
	/// emits proper `wl_surface.damage_buffer` requests instead of its
	/// `INT32_MAX`-everywhere sentinel. Falls back to plain `swap_buffers`
	/// when the extension is unavailable — that path still works on
	/// cooperative compositors but trips over the sentinel on others (see the
	/// crate-private `SwapBuffersWithDamageFn` type alias above for the
	/// underlying rationale).
	///
	/// Each rect is `(x, y, width, height)` in **EGL window coordinates**
	/// (origin at the bottom-left, in physical pixels). Callers that work in
	/// top-left screen coords must flip Y before passing them in. For
	/// full-surface damage, `(0, 0, w, h)` is correct in either convention.
	pub fn swap_buffers_with_damage(
		&self, surface: &EglSurface, rects: &[ ( i32, i32, i32, i32 ) ],
	) -> Result<(), String>
	{
		let Some( func ) = self.swap_with_damage else
		{
			return self.swap_buffers( surface );
		};
		// Pack rects into a contiguous i32 array as required by the extension.
		let mut packed: Vec<egl::Int> = Vec::with_capacity( rects.len() * 4 );
		for &( x, y, w, h ) in rects
		{
			packed.extend_from_slice( &[ x, y, w, h ] );
		}
		// SAFETY: `func` was resolved in `EglContext::new` via the typed
		// `Option<SwapBuffersWithDamageFn>` slot, so its signature is
		// known. `display` / `surface.surface` belong to the live `&self`
		// / `&surface` borrows. `packed.as_ptr()` is valid for `rects.len()
		// * 4` `egl::Int` reads — we just built it with that exact length.
		let ok = unsafe
		{
			func(
				self.display.as_ptr(),
				surface.surface.as_ptr(),
				packed.as_ptr(),
				rects.len() as egl::Int,
			)
		};
		if ok == egl::TRUE
		{
			Ok( () )
		} else {
			Err( "eglSwapBuffersWithDamage failed".to_string() )
		}
	}
}

impl EglSurface
{
	/// Resize the backing `wl_egl_window` to `width` x `height` physical
	/// pixels, so the next swap allocates a buffer of the new size. Must be
	/// called whenever the Wayland surface changes size. Dimensions are
	/// clamped to a minimum of 1; this only resizes the buffer and does not
	/// set the GL viewport — the caller updates `glViewport` separately.
	pub fn resize( &self, width: i32, height: i32 )
	{
		self.egl_window.resize( width.max( 1 ), height.max( 1 ), 0, 0 );
	}
}

impl EglOffscreenContext
{
	/// Create a runtime-free EGL context suitable for `Canvas::new_gles`.
	pub fn new() -> Result<Self, String>
	{
		if std::env::var( "LTK_FORCE_SOFTWARE" ).map( |v| v != "0" ).unwrap_or( false )
		{
			return Err( "LTK_FORCE_SOFTWARE=1".to_string() );
		}

		// SAFETY: same as `EglContext::new` — `dlopen` of the system
		// `libEGL.so`. Same tradeoff and rationale apply.
		let egl: Arc<EglInstance> = Arc::new(
			unsafe { EglInstance::load_required() }
				.map_err( |e| format!( "load libEGL: {e:?}" ) )?,
		);

		let display = offscreen_display( &egl )?;
		egl.initialize( display )
			.map_err( |e| format!( "eglInitialize: {e:?}" ) )?;
		egl.bind_api( egl::OPENGL_ES_API )
			.map_err( |e| format!( "eglBindAPI: {e:?}" ) )?;

		let config_attribs = [
			egl::SURFACE_TYPE,    egl::PBUFFER_BIT,
			egl::RED_SIZE,        8,
			egl::GREEN_SIZE,      8,
			egl::BLUE_SIZE,       8,
			egl::ALPHA_SIZE,      8,
			egl::RENDERABLE_TYPE, egl::OPENGL_ES2_BIT,
			egl::NONE,
		];
		let config = egl.choose_first_config( display, &config_attribs )
			.map_err( |e| format!( "eglChooseConfig: {e:?}" ) )?
			.ok_or_else( || "no compatible offscreen EGL config".to_string() )?;

		let ( context, version ) = match try_create_context( &egl, display, config, 3 )
		{
			Ok( ctx ) => ( ctx, GlesVersion::V3 ),
			Err( _ )  => match try_create_context( &egl, display, config, 2 )
			{
				Ok( ctx ) => ( ctx, GlesVersion::V2 ),
				Err( e )  => return Err( format!( "eglCreateContext (ES2/ES3): {e:?}" ) ),
			},
		};

		let pbuffer_attribs = [
			egl::WIDTH,  1,
			egl::HEIGHT, 1,
			egl::NONE,
		];
		let surface = match egl.create_pbuffer_surface( display, config, &pbuffer_attribs )
		{
			Ok( surface ) => surface,
			Err( e ) =>
			{
				let _ = egl.destroy_context( display, context );
				let _ = egl.terminate( display );
				return Err( format!( "eglCreatePbufferSurface: {e:?}" ) );
			},
		};

		if let Err( e ) = egl.make_current( display, Some( surface ), Some( surface ), Some( context ) )
		{
			let _ = egl.destroy_surface( display, surface );
			let _ = egl.destroy_context( display, context );
			let _ = egl.terminate( display );
			return Err( format!( "eglMakeCurrent: {e:?}" ) );
		}

		let egl_for_loader = Arc::clone( &egl );
		// SAFETY: as in `EglContext::make_current`. The `make_current`
		// call above has already established the EGL context as current
		// on this thread, so `glGetString( GL_VERSION )` (called inside
		// `from_loader_function`) is well-defined.
		let gl = Arc::new( unsafe
		{
			glow::Context::from_loader_function( move |name|
			{
				egl_for_loader.get_proc_address( name )
					.map( |p| p as *const _ )
					.unwrap_or( std::ptr::null() )
			} )
		} );

		log_backend_once( match version
		{
			GlesVersion::V3 => "GLES3",
			GlesVersion::V2 => "GLES2",
		} );

		Ok( Self { egl, display, config, context, surface, version, gl } )
	}

	/// Make this context current on the calling thread.
	pub fn make_current( &self ) -> Result<(), String>
	{
		self.egl.make_current(
			self.display,
			Some( self.surface ),
			Some( self.surface ),
			Some( self.context ),
		).map_err( |e| format!( "eglMakeCurrent: {e:?}" ) )
	}

	/// The shared `glow::Context` for this offscreen context. Built eagerly in
	/// [`Self::new`] (which leaves the context current), so it is always ready.
	pub fn gl( &self ) -> &Arc<glow::Context> { &self.gl }
	/// The GLES major version obtained (ES3 preferred, ES2 fallback).
	pub fn version( &self ) -> GlesVersion { self.version }
	/// The `EGLConfig` the context and pbuffer were created against.
	pub fn config( &self ) -> egl::Config { self.config }
}

fn try_create_context(
	egl: &EglInstance, display: egl::Display, config: egl::Config, major: i32,
) -> Result<egl::Context, egl::Error>
{
	let attribs = [
		egl::CONTEXT_CLIENT_VERSION, major,
		egl::NONE,
	];
	egl.create_context( display, config, None, &attribs )
}

fn offscreen_display( egl: &EglInstance ) -> Result<egl::Display, String>
{
	// SAFETY: `DEFAULT_DISPLAY` is the EGL sentinel for "the default display
	// for the current platform" and is always a valid argument to
	// `eglGetDisplay` — the spec guarantees it returns either a valid
	// display handle or `EGL_NO_DISPLAY`. No raw pointer is dereferenced
	// in this crate.
	unsafe { egl.get_display( egl::DEFAULT_DISPLAY ) }
		.ok_or_else( || "eglGetDisplay(DEFAULT_DISPLAY) returned NULL".to_string() )
}

/// Log the SOFTWARE fallback once, with the reason. The GPU branch logs
/// from [`EglContext::new`].
pub fn log_software_fallback( reason: &str )
{
	log_backend_once( &format!( "SOFTWARE ({reason})" ) );
}

fn log_backend_once( label: &str )
{
	static ONCE: Once = Once::new();
	ONCE.call_once( ||
	{
		eprintln!( "[ltk] render backend: {label}" );
	} );
}
