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

//! Input-transparent child surfaces ([`crate::app::SubsurfaceSpec`]).
//!
//! Each spec maps to one `wl_subsurface` sized to the main surface, with an
//! empty input region so all input falls through to the parent — the host
//! keeps a single gesture/input model. The content buffer is rasterised only
//! when the size or the spec's `content_version` changes; a position change
//! emits `wl_subsurface.set_position` plus a bare parent commit, so an animated
//! slide costs a compositor recomposite, not a client re-raster.

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

use smithay_client_toolkit::compositor::CompositorState;
use smithay_client_toolkit::shm::Shm;
use smithay_client_toolkit::shm::slot::SlotPool;
use smithay_client_toolkit::reexports::client::protocol::wl_shm;
use smithay_client_toolkit::reexports::client::protocol::wl_subsurface::WlSubsurface;
use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface;

use crate::app::{ App, SubsurfaceParent };
use crate::egl_context::{ EglContext, EglSurface };
use crate::draw::DrawCtx;
use crate::draw::chrome::apply_input_region;
use crate::draw::layout_and_draw;
use crate::render::Canvas;
use crate::types::Rect;
use crate::widget::Element;

use super::frame::pick_shm_format;
use super::AppData;

/// Convert a layout (physical) position to the logical position the
/// compositor expects for `wl_subsurface.set_position`.
fn to_logical( x: i32, y: i32, scale: u32 ) -> ( i32, i32 )
{
	let s = scale.max( 1 ) as i32;
	( x / s, y / s )
}

/// Live state for one subsurface: its Wayland objects, its own SHM pool and
/// canvas, and the last (size, version, position) we committed so the per-frame
/// pass can tell a re-raster from a cheap reposition.
pub( crate ) struct SubsurfaceSlot
{
	subsurface:   WlSubsurface,
	surface:      WlSurface,
	/// SHM pool for the software raster path. `None` on the GLES path.
	pool:         Option<SlotPool>,
	/// EGL window surface for the GLES raster path (so the `surface-panel`
	/// backdrop blur, a GLES-only pass, renders). `None` on the software path.
	egl_surface:  Option<EglSurface>,
	canvas:       Option<Canvas>,
	last_pos:     ( i32, i32 ),
	last_size:    ( u32, u32 ),
	last_version: u64,
	rastered:     bool,
	parent:       SubsurfaceParent,
}

impl SubsurfaceSlot
{
	fn destroy( mut self )
	{
		// Drop the EGL surface before the wl_surface its wl_egl_window wraps.
		self.egl_surface = None;
		self.subsurface.destroy();
		self.surface.destroy();
	}
}

/// Resolve a subsurface's parent surface to `( wl_surface, phys_w, phys_h,
/// scale )`. Returns `None` when an [`SubsurfaceParent::Overlay`] target is
/// absent, not yet configured, or zero-sized — the caller skips the spec for
/// that frame. The `WlSurface` is cloned (an `Arc`) so the render / reposition
/// loop can run without holding a borrow on `data.overlays` / `data.main`.
fn resolve_parent<A: App>( data: &AppData<A>, parent: SubsurfaceParent ) -> Option<( WlSurface, u32, u32, u32 )>
{
	let ss = match parent
	{
		SubsurfaceParent::Main          => &data.main,
		SubsurfaceParent::Overlay( id ) => data.overlays.get( &id )?,
	};
	if !ss.configured { return None; }
	if ss.width == 0 || ss.height == 0 { return None; }
	let surface = ss.surface.try_wl_surface()?.clone();
	let scale   = ss.scale_factor.max( 1 ) as u32;
	Some( ( surface, ss.width * scale, ss.height * scale, scale ) )
}

/// Record a parent surface that needs a commit this frame, deduped by parent
/// identity so each is committed once.
fn mark_parent_dirty( list: &mut Vec<( SubsurfaceParent, WlSurface )>, parent: SubsurfaceParent, wl: &WlSurface )
{
	if !list.iter().any( |( p, _ )| *p == parent )
	{
		list.push( ( parent, wl.clone() ) );
	}
}

/// Reconcile the live subsurfaces against [`App::subsurfaces`], render content
/// where it changed and reposition where it moved. Cheap when nothing but a
/// position changed. Each spec is composited under its own parent surface
/// (main or an overlay), so a slide can ride above app windows. Must run after
/// the parent surface is configured.
pub( crate ) fn reconcile_subsurfaces<A: App>( data: &mut AppData<A> )
{
	if data.subcompositor.is_none() { return; }
	if !data.main.configured { return; }

	let ( format, swap_rb ) = pick_shm_format( &data.shm );
	let specs: Vec<crate::app::SubsurfaceSpec<A::Message>> = data.app.subsurfaces();

	// Parents touched this frame; each committed once at the end so the
	// placement / mapping actually lands.
	let mut dirty_parents: Vec<( SubsurfaceParent, WlSurface )> = Vec::new();

	// Drop subsurfaces whose id disappeared from the spec list.
	let present: std::collections::HashSet<crate::app::SubsurfaceId> =
		specs.iter().map( |s| s.id ).collect();
	let stale: Vec<crate::app::SubsurfaceId> = data.subsurfaces.keys()
		.copied()
		.filter( |id| !present.contains( id ) )
		.collect();
	for id in stale
	{
		if let Some( slot ) = data.subsurfaces.remove( &id )
		{
			let parent = slot.parent;
			slot.destroy();
			if let Some( ( wl, _, _, _ ) ) = resolve_parent( data, parent )
			{
				mark_parent_dirty( &mut dirty_parents, parent, &wl );
			}
		}
	}

	for spec in &specs
	{
		let Some( ( parent_wl, pw, ph, scale ) ) = resolve_parent( data, spec.parent ) else { continue };

		// Create on first sight.
		if !data.subsurfaces.contains_key( &spec.id )
		{
			let ( subsurface, surface ) = {
				let sc = data.subcompositor.as_ref().unwrap();
				sc.create_subsurface( parent_wl.clone(), &data.qh )
			};
			// Independent buffer commits; placement still applies on the
			// parent commit we issue below.
			subsurface.set_desync();
			let ( lx, ly ) = to_logical( spec.x, spec.y, scale );
			subsurface.set_position( lx, ly );
			data.subsurfaces.insert( spec.id, SubsurfaceSlot {
				subsurface,
				surface,
				pool:         None,
				egl_surface:  None,
				canvas:       None,
				last_pos:     ( spec.x, spec.y ),
				last_size:    ( 0, 0 ),
				last_version: u64::MAX,
				rastered:     false,
				parent:       spec.parent,
			} );
			mark_parent_dirty( &mut dirty_parents, spec.parent, &parent_wl );
		}

		// Tracked on the slot (not the canvas) because the GLES canvas lives
		// in `data.subsurface_gles_canvas`, shared across all subsurfaces.
		let size_changed = data.subsurfaces.get( &spec.id )
			.map( |s| s.last_size != ( pw, ph ) )
			.unwrap_or( true );
		let needs_raster = {
			let slot = data.subsurfaces.get( &spec.id ).unwrap();
			!slot.rastered || size_changed || slot.last_version != spec.content_version
		};

		if needs_raster
		{
			// GLES only when the spec asks for it (glass content); otherwise
			// the cheaper-to-create software rasteriser.
			let egl = if spec.gpu { data.egl_context.as_ref() } else { None };
			let slot = data.subsurfaces.get_mut( &spec.id ).unwrap();
			render_slot::<A::Message>(
				slot, egl, &mut data.subsurface_gles_canvas,
				&data.shm, &data.compositor_state, &spec.view,
				pw, ph, scale, format, swap_rb, size_changed,
			);
			slot.rastered     = true;
			slot.last_version = spec.content_version;
			slot.last_size    = ( pw, ph );
			mark_parent_dirty( &mut dirty_parents, spec.parent, &parent_wl );
		}

		let slot = data.subsurfaces.get_mut( &spec.id ).unwrap();
		if slot.last_pos != ( spec.x, spec.y )
		{
			let ( lx, ly ) = to_logical( spec.x, spec.y, scale );
			slot.subsurface.set_position( lx, ly );
			// Desync subsurface state (the position) is applied on the child
			// surface's own commit, not the parent's; commit it here so the
			// move actually lands. The parent commit below applies placement.
			slot.surface.commit();
			slot.last_pos = ( spec.x, spec.y );
			mark_parent_dirty( &mut dirty_parents, spec.parent, &parent_wl );
		}
	}

	// Applies pending subsurface placement / mapping without re-attaching the
	// parent buffer — the cheap per-frame move.
	for ( _, wl ) in dirty_parents
	{
		wl.commit();
	}
}

/// Input-transparent subsurfaces never carry focus / hover / scroll state,
/// so they draw with an empty context.
fn empty_draw_ctx<Msg: Clone>() -> DrawCtx<Msg>
{
	DrawCtx
	{
		focused_idx:            None,
		hovered_idx:            None,
		pressed_idx:            None,
		cursor_state:           HashMap::new(),
		selection_anchor:       HashMap::new(),
		widget_rects:           Vec::new(),
		debug_layout:           false,
		scroll_offsets:         HashMap::new(),
		scroll_rects:           Vec::new(),
		scroll_canvases:        HashMap::new(),
		scroll_navigable_items: HashMap::new(),
		previous_widget_rects:  Vec::new(),
		accessible_extras:      Vec::new(),
		live_depth:             0,
	}
}

/// Raster a subsurface's content. Uses the GLES path when an [`EglContext`]
/// is available — only there does the `surface-panel` backdrop blur render;
/// the software fallback paints the same widgets without the blur.
#[ allow( clippy::too_many_arguments ) ]
fn render_slot<Msg: Clone>(
	slot:        &mut SubsurfaceSlot,
	egl_ctx:     Option<&Arc<EglContext>>,
	gles_canvas: &mut Option<Canvas>,
	shm:         &Shm,
	compositor:  &CompositorState,
	view:        &Element<Msg>,
	pw:          u32,
	ph:          u32,
	scale:       u32,
	shm_format:  wl_shm::Format,
	swap_rb:     bool,
	size_changed: bool,
)
{
	if let Some( ctx ) = egl_ctx
	{
		render_slot_gpu::<Msg>( slot, ctx, gles_canvas, compositor, view, pw, ph, scale, size_changed );
	}
	else
	{
		render_slot_software::<Msg>( slot, shm, compositor, view, pw, ph, scale, shm_format, swap_rb, size_changed );
	}
}

#[ allow( clippy::too_many_arguments ) ]
fn render_slot_gpu<Msg: Clone>(
	slot:        &mut SubsurfaceSlot,
	egl_ctx:     &Arc<EglContext>,
	gles_canvas: &mut Option<Canvas>,
	compositor:  &CompositorState,
	view:        &Element<Msg>,
	pw:          u32,
	ph:          u32,
	scale:       u32,
	size_changed: bool,
)
{
	// (Re)create the EGL window surface on first sight or a size change.
	if slot.egl_surface.is_none() || size_changed
	{
		match egl_ctx.create_surface( &slot.surface, pw as i32, ph as i32 )
		{
			Ok( es ) => slot.egl_surface = Some( es ),
			Err( e ) =>
			{
				eprintln!( "ltk: subsurface EGL surface creation failed: {e}" );
				return;
			}
		}
	}
	let es = slot.egl_surface.as_ref().unwrap();
	if egl_ctx.make_current( es ).is_err() { return; }

	// The canvas (and its compiled shader programs) is shared across all
	// subsurface rasters, so a lazily re-created panel doesn't recompile.
	let canvas = gles_canvas.get_or_insert_with( ||
	{
		let mut c = Canvas::new_gles( Arc::clone( egl_ctx.gl() ), egl_ctx.version, pw, ph );
		c.set_dpi_scale( scale as f32 );
		if let Some( reg ) = crate::theme::build_font_registry()
		{
			c.set_font_registry( Arc::new( reg ) );
		}
		c
	} );
	if canvas.size() != ( pw, ph )
	{
		canvas.resize( pw, ph );
		canvas.set_dpi_scale( scale as f32 );
	}
	canvas.clear_clip();
	canvas.clear();

	let screen_rect = Rect { x: 0.0, y: 0.0, width: pw as f32, height: ph as f32 };
	let mut ctx = empty_draw_ctx::<Msg>();
	layout_and_draw::<Msg>( view, canvas, screen_rect, &mut ctx, 0 );
	canvas.present();

	let wl = &slot.surface;
	wl.set_buffer_scale( scale as i32 );
	// Empty input region: pointer/touch fall through to the parent.
	apply_input_region( wl, compositor, Some( &[] ), scale );
	// Implicitly commits the wl_surface with the freshly rendered buffer.
	let _ = egl_ctx.swap_buffers_with_damage( es, &[ ( 0, 0, pw as i32, ph as i32 ) ] );
}

#[ allow( clippy::too_many_arguments ) ]
fn render_slot_software<Msg: Clone>(
	slot:        &mut SubsurfaceSlot,
	shm:         &Shm,
	compositor:  &CompositorState,
	view:        &Element<Msg>,
	pw:          u32,
	ph:          u32,
	scale:       u32,
	shm_format:  wl_shm::Format,
	swap_rb:     bool,
	size_changed: bool,
)
{
	if slot.pool.is_none() || size_changed
	{
		match SlotPool::new( ( pw * ph * 4 ) as usize, shm )
		{
			Ok( p )  => slot.pool = Some( p ),
			Err( _ ) => return,
		}
	}
	let pool   = slot.pool.as_mut().unwrap();
	let stride = pw * 4;
	let ( buffer, canvas_buf ) = match pool.create_buffer(
		pw as i32, ph as i32, stride as i32, shm_format,
	)
	{
		Ok( r )  => r,
		Err( _ ) => return,
	};

	let canvas = slot.canvas.get_or_insert_with( || {
		let mut c = Canvas::new( pw, ph );
		c.set_dpi_scale( scale as f32 );
		if let Some( reg ) = crate::theme::build_font_registry()
		{
			c.set_font_registry( Arc::new( reg ) );
		}
		c
	} );
	if canvas.size() != ( pw, ph )
	{
		canvas.resize( pw, ph );
		canvas.set_dpi_scale( scale as f32 );
	}
	canvas.clear_clip();
	canvas.clear();

	let screen_rect = Rect { x: 0.0, y: 0.0, width: pw as f32, height: ph as f32 };
	let mut ctx = empty_draw_ctx::<Msg>();
	layout_and_draw::<Msg>( view, canvas, screen_rect, &mut ctx, 0 );

	canvas.write_to_wayland_buf( canvas_buf, swap_rb );

	let wl = &slot.surface;
	if buffer.attach_to( wl ).is_err() { return; }
	wl.set_buffer_scale( scale as i32 );
	wl.damage_buffer( 0, 0, pw as i32, ph as i32 );
	// Empty input region: pointer/touch fall through to the parent.
	apply_input_region( wl, compositor, Some( &[] ), scale );
	wl.commit();
}
