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

//! Runtime-free UI surface primitives.
//!
//! This module exposes the part of ltk that is useful outside `ltk::run`:
//! widget tree layout, drawing into a `Canvas`, hit-test rect snapshots, and
//! damage tracking. It deliberately contains no Wayland client event loop,
//! layer-shell, xdg-shell, SHM pool, or frame-callback logic.

use std::collections::HashMap;
use std::ffi::c_void;
use std::sync::Arc;

pub use crate::gles_render::{ BorrowedGlesTexture, GlesVersion };
pub use crate::render::Canvas;
pub use crate::widget::{ LaidOutWidget, WidgetHandlers };

use crate::draw::{ self, DrawCtx };
use crate::egl_context::EglOffscreenContext;
use crate::types::{ Color, Point, Rect };
use crate::widget::Element;

/// Options for one runtime-free render pass.
#[ derive( Debug, Clone, Copy ) ]
pub struct RenderOptions
{
	/// Physical-pixel bounds to lay out the tree into.
	pub bounds:       Rect,
	/// Background fill. Use [`Color::TRANSPARENT`] for a transparent target.
	pub background:   Color,
	/// Draw red debug rectangles around laid-out interactive widgets.
	pub debug_layout: bool,
}

impl RenderOptions
{
	/// Render into the full `width × height` canvas with a transparent
	/// background and debug layout disabled.
	pub fn full_canvas( width: u32, height: u32 ) -> Self
	{
		Self
		{
			bounds:       Rect { x: 0.0, y: 0.0, width: width as f32, height: height as f32 },
			background:   Color::TRANSPARENT,
			debug_layout: false,
		}
	}

	/// Set the background fill for the render pass.
	pub fn background( mut self, color: Color ) -> Self
	{
		self.background = color;
		self
	}

	/// Enable or disable debug layout rectangles.
	pub fn debug_layout( mut self, yes: bool ) -> Self
	{
		self.debug_layout = yes;
		self
	}
}

/// Result of rendering a tree into a [`UiSurface`].
#[ derive( Debug, Clone ) ]
pub struct RenderOutput
{
	/// Damage rects in physical pixels. Empty means "damage the full bounds".
	pub damage_rects: Vec<Rect>,
	/// True when the caller should treat the whole render bounds as dirty.
	pub full_redraw:  bool,
}

/// A retained rendering target for code that wants ltk widgets without
/// `ltk::run`.
///
/// A compositor can keep one `UiSurface` per decoration or panel, mutate
/// focus/hover/pressed state from its own input routing, then call
/// [`Self::render`] whenever it decides a repaint is needed.
// Field declaration order is load-bearing: Rust drops fields top-to-bottom,
// so `canvas` is dropped before `egl_context`. That matters because
// `Canvas::Gles::Drop` releases textures / FBOs / shader programs through
// glow, which requires the matching GL context to be current — which is what
// `Drop for UiSurface` ensures by calling `make_owned_gles_current()` first.
// If `egl_context` were declared above `canvas`, the EGL context would be
// torn down first and `canvas`'s GL releases would silently leak / corrupt
// state. Do not reorder these two fields without preserving that property.
pub struct UiSurface<Msg: Clone>
{
	canvas:          Canvas,
	egl_context:     Option<EglOffscreenContext>,
	focused_idx:     Option<usize>,
	hovered_idx:     Option<usize>,
	pressed_idx:     Option<usize>,
	prev_focused:    Option<usize>,
	prev_hovered:    Option<usize>,
	prev_pressed:    Option<usize>,
	widget_rects:    Vec<LaidOutWidget<Msg>>,
	cursor_state:    HashMap<usize, usize>,
	selection_anchor: HashMap<usize, usize>,
	scroll_offsets:  HashMap<usize, ( f32, f32 )>,
	scroll_canvases: HashMap<usize, Canvas>,
	scroll_navigable_items: HashMap<usize, Vec<( usize, f32, f32 )>>,
	content_dirty:   bool,
}

impl<Msg: Clone> UiSurface<Msg>
{
	/// Create a software-backed surface of the given physical size.
	///
	/// This is intentionally conservative for compositor integrations: an
	/// embedder typically already owns an EGL/GLES context, so `new` must
	/// not allocate a hidden offscreen context per decoration. Use
	/// [`Self::from_gles_context`] when the caller can provide an
	/// already-current GL context.
	pub fn new( width: u32, height: u32 ) -> Self
	{
		Self::new_software( width, height )
	}

	/// Create a software-backed surface explicitly.
	pub fn new_software( width: u32, height: u32 ) -> Self
	{
		Self::from_canvas( Canvas::new( width, height ) )
	}

	/// Create a GLES-backed surface with an ltk-owned offscreen EGL context.
	///
	/// The context is made current before render/resize operations. This is
	/// useful for runtime-free rendering; compositors that already own a GL
	/// context can instead build a `Canvas::new_gles(...)` and pass it to
	/// [`Self::from_canvas`].
	pub fn try_new_gles( width: u32, height: u32 ) -> Result<Self, String>
	{
		let egl_context = EglOffscreenContext::new()?;
		egl_context.make_current()?;
		let canvas = Canvas::new_gles(
			Arc::clone( egl_context.gl() ),
			egl_context.version(),
			width,
			height,
		);
		Ok( Self::from_canvas_with_egl_context( canvas, Some( egl_context ) ) )
	}

	/// Create a GLES-backed surface from a caller-owned, already-current GL
	/// context.
	///
	/// This is the path intended for compositor embedders: no EGL display,
	/// EGL context, or pbuffer is allocated by ltk. The caller must keep
	/// the underlying GL context alive and current whenever this surface
	/// is created, rendered, resized, or dropped.
	pub fn from_gles_context(
		gl:      Arc<glow::Context>,
		version: GlesVersion,
		width:   u32,
		height:  u32,
	) -> Self
	{
		Self::from_canvas( Canvas::new_gles( gl, version, width, height ) )
	}

	/// Create a GLES-backed surface from the GL function loader of the current
	/// context.
	///
	/// This is useful for renderers such as Smithay's `GlesRenderer`, which
	/// keep their own EGL context and expose custom GL access through a
	/// callback. This constructor does not allocate an EGL context; it only
	/// builds ltk's `glow` dispatch table and GPU canvas resources in the
	/// context that is current while this function runs.
	///
	/// # Safety
	///
	/// `loader` must resolve symbols for the GL context that is current on this
	/// thread. That same context must remain alive, and must be made current
	/// before any call that touches the returned surface — **including the
	/// implicit destructor**: dropping the `UiSurface` releases GPU resources
	/// (textures, FBOs, programs) through the caller-owned context and will
	/// leak / corrupt state if that context is not current at drop time.
	pub unsafe fn from_current_gles_loader<F>(
		mut loader: F,
		version:    GlesVersion,
		width:      u32,
		height:     u32,
	) -> Self
	where
		F: FnMut( &str ) -> *const c_void,
	{
		// SAFETY: forwarded from `from_current_gles_loader`'s own `# Safety`
		// contract — `loader` resolves symbols for a context 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| loader( name ) ) } );
		Self::from_gles_context( gl, version, width, height )
	}

	/// Wrap an existing canvas. This is the hook for compositor-owned GPU
	/// targets once the caller provides an already-current GL context. If
	/// `canvas` is `Canvas::Gles`, the caller remains responsible for making the
	/// matching GL context current before calling methods that touch the
	/// canvas — **including the implicit destructor**: dropping the
	/// `UiSurface` releases GPU resources (textures, FBOs, programs) through
	/// the caller-owned context and will leak / corrupt state if that context
	/// is not current at drop time.
	pub fn from_canvas( canvas: Canvas ) -> Self
	{
		Self::from_canvas_with_egl_context( canvas, None )
	}

	fn from_canvas_with_egl_context(
		canvas:      Canvas,
		egl_context: Option<EglOffscreenContext>,
	) -> Self
	{
		Self
		{
			canvas,
			egl_context,
			focused_idx:     None,
			hovered_idx:     None,
			pressed_idx:     None,
			prev_focused:    None,
			prev_hovered:    None,
			prev_pressed:    None,
			widget_rects:    Vec::new(),
			cursor_state:    HashMap::new(),
			selection_anchor: HashMap::new(),
			scroll_offsets:  HashMap::new(),
			scroll_canvases: HashMap::new(),
			scroll_navigable_items: HashMap::new(),
			content_dirty:   true,
		}
	}

	/// Access the backing canvas after a render pass.
	pub fn canvas( &self ) -> &Canvas
	{
		&self.canvas
	}

	/// Mutable access to the backing canvas for compositor-specific upload or
	/// presentation code. Mark content dirty afterwards if external drawing
	/// changes what ltk should preserve across partial redraws.
	pub fn canvas_mut( &mut self ) -> &mut Canvas
	{
		self.make_owned_gles_current();
		&mut self.canvas
	}

	/// Current canvas size in physical pixels.
	pub fn size( &self ) -> ( u32, u32 )
	{
		self.canvas.size()
	}

	/// Resize the backing canvas and force the next render to redraw fully.
	pub fn resize( &mut self, width: u32, height: u32 )
	{
		self.make_owned_gles_current();
		self.canvas.resize( width, height );
		self.mark_content_dirty();
	}

	/// Set the DPI scale used for text and font metrics.
	pub fn set_dpi_scale( &mut self, scale: f32 )
	{
		self.make_owned_gles_current();
		self.canvas.set_dpi_scale( scale );
		self.mark_content_dirty();
	}

	/// Mark the next render as content-changing, forcing a full repaint.
	pub fn mark_content_dirty( &mut self )
	{
		self.content_dirty = true;
	}

	/// Current laid-out interactive widgets from the last render.
	pub fn widget_rects( &self ) -> &[ LaidOutWidget<Msg> ]
	{
		&self.widget_rects
	}

	/// Hit-test a physical point against the last rendered widget rects.
	pub fn hit_test( &self, pos: Point ) -> Option<usize>
	{
		crate::tree::find_widget_at( &self.widget_rects, pos )
	}

	/// Lookup a laid-out widget by flat index.
	pub fn widget( &self, flat_idx: usize ) -> Option<&LaidOutWidget<Msg>>
	{
		crate::tree::find_widget( &self.widget_rects, flat_idx )
	}

	/// Lookup the handler snapshot for a laid-out widget.
	pub fn handlers( &self, flat_idx: usize ) -> Option<&WidgetHandlers<Msg>>
	{
		crate::tree::find_handlers( &self.widget_rects, flat_idx )
	}

	/// Update keyboard focus state. This is interaction-only, so the next
	/// render can use partial damage when layout/content did not change.
	pub fn set_focused( &mut self, idx: Option<usize> )
	{
		self.focused_idx = idx;
	}

	/// Update hover state. This is interaction-only.
	pub fn set_hovered( &mut self, idx: Option<usize> )
	{
		self.hovered_idx = idx;
	}

	/// Update pressed state. This is interaction-only.
	pub fn set_pressed( &mut self, idx: Option<usize> )
	{
		self.pressed_idx = idx;
	}

	/// Flat index of the keyboard-focused widget, or `None` when nothing holds focus.
	pub fn focused( &self ) -> Option<usize> { self.focused_idx }
	/// Flat index of the hovered widget, or `None` when the pointer is over no interactive widget.
	pub fn hovered( &self ) -> Option<usize> { self.hovered_idx }
	/// Flat index of the pressed widget, or `None` when no widget is held down.
	pub fn pressed( &self ) -> Option<usize> { self.pressed_idx }

	/// Render `element` into the backing canvas.
	///
	/// This does not commit, swap buffers, request frame callbacks, or talk to
	/// Wayland. The caller owns presentation and frame pacing.
	pub fn render( &mut self, element: &Element<Msg>, options: RenderOptions ) -> RenderOutput
	{
		self.make_owned_gles_current();
		let ( width, height ) = self.canvas.size();

		let damage_rects = if self.content_dirty || self.widget_rects.is_empty()
		{
			Vec::new()
		}
		else
		{
			draw::compute_interaction_dirty_rects(
				&self.widget_rects,
				self.prev_focused, self.prev_hovered, self.prev_pressed,
				self.focused_idx,  self.hovered_idx,  self.pressed_idx,
				width,
				height,
			)
		};

		let use_partial = !self.content_dirty && !damage_rects.is_empty();
		// Only `compute_damage` (called below in the !use_partial branch)
		// needs the previous frame's rects, and it consumes them by reference.
		// The clone is therefore deferred to that branch — the partial-damage
		// path (typical for hover / focus / press transitions) skips it
		// entirely, saving one Vec<LaidOutWidget> clone per frame.
		let old_rects: Vec<LaidOutWidget<Msg>> = if use_partial
		{
			Vec::new()
		}
		else
		{
			self.widget_rects.clone()
		};
		if use_partial
		{
			self.canvas.set_clip_rects( &damage_rects );
			if options.background.a > 0.0
			{
				self.canvas.fill( options.background );
			}
			else
			{
				self.canvas.clear_rects_transparent( &damage_rects );
			}
		}
		else
		{
			self.canvas.clear_clip();
			if options.background.a > 0.0
			{
				self.canvas.fill( options.background );
			}
			else
			{
				self.canvas.clear();
			}
		}

		let mut ctx: DrawCtx<Msg> = DrawCtx
		{
			focused_idx:     self.focused_idx,
			hovered_idx:     self.hovered_idx,
			pressed_idx:     self.pressed_idx,
			cursor_state:    std::mem::take( &mut self.cursor_state ),
			selection_anchor: std::mem::take( &mut self.selection_anchor ),
			widget_rects:    Vec::new(),
			debug_layout:    options.debug_layout && !use_partial,
			scroll_offsets:  std::mem::take( &mut self.scroll_offsets ),
			scroll_rects:    Vec::new(),
			scroll_canvases: std::mem::take( &mut self.scroll_canvases ),
			scroll_navigable_items: std::mem::take( &mut self.scroll_navigable_items ),
			previous_widget_rects: self.widget_rects.clone(),
			accessible_extras: Vec::new(),
			live_depth: 0,
		};

		draw::layout_and_draw( element, &mut self.canvas, options.bounds, &mut ctx, 0 );

		if ctx.debug_layout && !use_partial
		{
			for w in &ctx.widget_rects
			{
				self.canvas.stroke_rect( w.rect, Color::rgb( 1.0, 0.0, 0.0 ), 1.5, 0.0 );
			}
		}
		self.canvas.clear_clip();

		let output_damage = if use_partial
		{
			damage_rects
		}
		else
		{
			draw::compute_damage(
				&old_rects,
				&ctx.widget_rects,
				self.prev_focused, self.prev_hovered, self.prev_pressed,
				self.focused_idx,  self.hovered_idx,  self.pressed_idx,
				width,
				height,
			)
		};

		self.prev_focused    = self.focused_idx;
		self.prev_hovered    = self.hovered_idx;
		self.prev_pressed    = self.pressed_idx;
		self.widget_rects    = ctx.widget_rects;
		self.cursor_state    = ctx.cursor_state;
		self.selection_anchor = ctx.selection_anchor;
		self.scroll_offsets  = ctx.scroll_offsets;
		// `ctx.scroll_rects` is intentionally dropped here. The `Scroll` widget
		// pushes its hit-test rects into the DrawCtx during the draw pass, but
		// `UiSurface` exposes no wheel-event routing API, so persisting them
		// across frames serves no consumer. If a future embedder needs to
		// dispatch wheel events through `UiSurface`, add a `scroll_rects()`
		// getter together with a `scroll_by( flat_idx, dy )` mutator and start
		// keeping the field again.
		self.scroll_canvases = ctx.scroll_canvases;
		self.scroll_navigable_items = ctx.scroll_navigable_items;
		self.content_dirty   = false;

		RenderOutput
		{
			full_redraw:  output_damage.is_empty(),
			damage_rects: output_damage,
		}
	}

	fn make_owned_gles_current( &self )
	{
		if let Some( egl_context ) = &self.egl_context
		{
			if let Err( e ) = egl_context.make_current()
			{
				log_make_current_failure_once( &e );
			}
		}
	}
}

fn log_make_current_failure_once( reason: &str )
{
	use std::sync::Once;
	static ONCE: Once = Once::new();
	ONCE.call_once( ||
	{
		eprintln!( "[ltk] core: eglMakeCurrent failed: {reason} (subsequent GL ops will silently no-op or render garbage)" );
	} );
}

impl<Msg: Clone> Drop for UiSurface<Msg>
{
	fn drop( &mut self )
	{
		self.make_owned_gles_current();
	}
}
