// 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;
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,
	pool:         Option<SlotPool>,
	canvas:       Option<Canvas>,
	last_pos:     ( i32, i32 ),
	last_version: u64,
	rastered:     bool,
}

impl SubsurfaceSlot
{
	fn destroy( self )
	{
		self.subsurface.destroy();
		self.surface.destroy();
	}
}

/// Reconcile the live subsurfaces against [`App::subsurfaces`], render content
/// where it changed and reposition where it moved. Cheap when nothing but a
/// position changed. 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 parent = match data.main.surface.try_wl_surface()
	{
		Some( s ) => s.clone(),
		None      => return,
	};

	let scale = data.main.scale_factor.max( 1 ) as u32;
	let ( w, h ) = ( data.main.width, data.main.height );
	if w == 0 || h == 0 { return; }
	let pw = w * scale;
	let ph = h * scale;
	let ( format, swap_rb ) = pick_shm_format( &data.shm );

	let specs: Vec<crate::app::SubsurfaceSpec<A::Message>> = data.app.subsurfaces();

	let mut parent_dirty = false;

	// 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 )
		{
			slot.destroy();
			parent_dirty = true;
		}
	}

	for spec in &specs
	{
		// 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.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,
				canvas:       None,
				last_pos:     ( spec.x, spec.y ),
				last_version: u64::MAX,
				rastered:     false,
			} );
			parent_dirty = true;
		}

		let size_changed = data.subsurfaces.get( &spec.id )
			.and_then( |s| s.canvas.as_ref() )
			.map( |c| c.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
		{
			let slot = data.subsurfaces.get_mut( &spec.id ).unwrap();
			render_slot::<A::Message>(
				slot, &data.shm, &data.compositor_state, &spec.view,
				pw, ph, scale, format, swap_rb, size_changed,
			);
			slot.rastered     = true;
			slot.last_version = spec.content_version;
			parent_dirty      = true;
		}

		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 );
			parent_dirty  = true;
		}
	}

	// Applies pending subsurface placement / mapping without re-attaching the
	// parent buffer — the cheap per-frame move.
	if parent_dirty
	{
		parent.commit();
	}
}

#[ allow( clippy::too_many_arguments ) ]
fn render_slot<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: 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,
	};
	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();
}
