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

use std::collections::HashSet;

use smithay_client_toolkit::
{
	compositor::Surface,
	shell::
	{
		WaylandSurface,
		xdg::
		{
			XdgPositioner, XdgSurface,
			popup::Popup,
		},
	},
};
use wayland_protocols::xdg::shell::client::xdg_positioner::
{
	Anchor as PositionerAnchor,
	ConstraintAdjustment,
	Gravity,
};

use crate::app::App;
use crate::types::{ Length, Rect };
use super::{ AppData, LayerConfig, SurfaceFocus, SurfaceKind, SurfaceState };

/// Sync `data.overlays` with the app's current `App::overlays()` spec list:
/// destroy any overlay whose id disappeared and create a fresh `SurfaceState`
/// for every id that just appeared. Created surfaces are materialized
/// immediately if an output is already available; otherwise they stay
/// [`SurfaceKind::Pending`] and the `new_output` handler will bring them up.
pub( super ) fn reconcile_overlays<A: App>( data: &mut AppData<A> )
{
	let mut specs = data.app.overlays();
	if let Some( ts ) = data.tooltip_overlay() { specs.push( ts ); }
	let next_ids: Vec<_> = specs.iter().map( |s| s.id ).collect();
	let wanted: HashSet<crate::app::OverlayId> = next_ids.iter().copied().collect();

	// Drop overlays that disappeared from the spec list. If the overlay had
	// an in-flight drag (long-press fired), migrate that state to `main` so
	// the motion / release that follows still routes through the app's drag
	// handlers — apps typically hide the overlay *because* of the long-press,
	// and we must not lose the drag state with the surface.
	let removed: Vec<_> = data.overlays.keys()
		.filter( |id| !wanted.contains( id ) )
		.copied()
		.collect();
	for id in removed
	{
		if let Some( ss ) = data.overlays.remove( &id )
		{
			if ss.gesture.long_press_fired
			{
				data.main.gesture.long_press_fired  = true;
				data.main.gesture.long_press_origin = ss.gesture.long_press_origin;
				// Keep the primary touch slot with the migrated drag so motion / release still route through the gesture machine.
				if data.main.primary_touch_id.is_none()
				{
					data.main.primary_touch_id = ss.primary_touch_id;
				}
			}
		}
	}
	// Clear any stale per-device focus pointing at a destroyed overlay.
	if let SurfaceFocus::Overlay( id ) = data.pointer_focus
	{
		if !data.overlays.contains_key( &id ) { data.pointer_focus = SurfaceFocus::Main; }
	}
	if let SurfaceFocus::Overlay( id ) = data.keyboard_focus
	{
		if !data.overlays.contains_key( &id ) { data.keyboard_focus = SurfaceFocus::Main; }
	}
	// Rewrite touch_focus entries pointing at a destroyed overlay to Main
	// rather than dropping them. Dropping would make subsequent motion/up
	// events for the same touch id default to Main *without* the long-press
	// drag state, turning a release into a stray tap.
	for f in data.touch_focus.values_mut()
	{
		if let SurfaceFocus::Overlay( id ) = *f
		{
			if !data.overlays.contains_key( &id ) { *f = SurfaceFocus::Main; }
		}
	}

	// Create overlays that just appeared. Uses field-level borrow splitting so
	// the shared borrows of `layer_shell` / `xdg_shell` / `compositor_state`
	// / `output_state` / `qh` can coexist with the mutable borrow of
	// `overlays` inside the loop.
	let layer_shell_opt = data.layer_shell.as_ref();
	let xdg_shell_opt   = data.xdg_shell.as_ref();
	let output_opt      = data.output_state.outputs().next();
	let cs              = &data.compositor_state;
	let qh              = &data.qh;
	let grab_seat   = data.seat_state.seats().next();
	let grab_serial = data.last_input_serial;
	// Snapshot the parent xdg_surface (only an xdg toplevel can host
	// xdg-popups; layer-shell parents would need a different code path
	// via `LayerSurface::get_popup`, which we do not currently support).
	let parent_xdg = match data.main.surface
	{
		SurfaceKind::Window( ref w ) => Some( w.xdg_surface().clone() ),
		_ => None,
	};
	let parent_scale_i = data.main.scale_factor.max( 1 );
	let parent_scale   = parent_scale_i as f32;
	// `OverlaySpec::size` carries `Length`s resolved against the main
	// surface's physical viewport (the app's layout space), so an overlay
	// sized with `Length::widget( … )` scales with the display exactly like
	// any other widget. `Length::px( n )` keeps a fixed size.
	let main_vp        = ( data.main.physical_width() as f32, data.main.physical_height() as f32 );
	let resolve_size   = move | s: &( Length, Length ) | -> ( u32, u32 )
	{
		(
			s.0.resolve( main_vp, Length::EM_BASE_DEFAULT ).max( 0.0 ).round() as u32,
			s.1.resolve( main_vp, Length::EM_BASE_DEFAULT ).max( 0.0 ).round() as u32,
		)
	};
	// `OverlaySpec::size` is physical pixels (the app's layout space); the
	// layer-shell `set_size` is logical. They only coincide at scale 1, so
	// convert here — without it a scale-2 overlay requests a surface twice
	// its intended size. `0 = fill` survives the divide.
	let to_logical_size = move | ( w, h ): ( u32, u32 ) | -> ( u32, u32 )
	{
		(
			( w as f32 / parent_scale ).round() as u32,
			( h as f32 / parent_scale ).round() as u32,
		)
	};
	// The exclusive zone measures the same band the surface occupies, so it
	// lives in the same space and needs the same conversion. `-1` (ignore
	// other zones) and `0` (reserve nothing) are sentinels, not distances.
	let to_logical_zone = move | z: i32 | -> i32
	{
		if z > 0 { ( z as f32 / parent_scale ).round() as i32 } else { z }
	};
	// Snapshot the previous-frame anchor lookup table so we can resolve
	// `anchor_widget_id` → `Rect` without holding a borrow on `data.main`
	// across the overlay-mut loop below.
	let main_widget_rects = &data.main.frame.widget_rects;
	let overlays_m = &mut data.overlays;
	for spec in &specs
	{
		let resolved_size = resolve_size( &spec.size );
		if let Some( ss ) = overlays_m.get_mut( &spec.id )
		{
			// Already-live overlay: propagate size changes to the layer
			// surface so apps can animate an overlay's dimensions (e.g. a
			// slide-down panel whose height grows each frame). The very
			// first size was applied at `materialize` time and recorded in
			// `last_requested_size`, so we only commit when it actually
			// differs. Commit is needed after `set_size` so the compositor
			// sends a configure; the usual `on_configure` path picks up the
			// new dimensions and drives the redraw. Popups don't grow /
			// shrink mid-life — close and reopen instead.
			let resolved_zone = to_logical_zone( spec.exclusive_zone );
			if resolved_size != ss.last_requested_size || resolved_zone != ss.last_requested_zone
			{
				if let SurfaceKind::Layer( ref layer_surface ) = ss.surface
				{
					if resolved_size != ss.last_requested_size
					{
						let ( lw, lh ) = to_logical_size( resolved_size );
						layer_surface.set_size( lw, lh );
						ss.last_requested_size = resolved_size;
					}
					if resolved_zone != ss.last_requested_zone
					{
						layer_surface.set_exclusive_zone( resolved_zone );
						ss.last_requested_zone = resolved_zone;
					}
					layer_surface.commit();
				}
			}
			// Compare the anchor at integer logical-pixel resolution:
			// floating-point jitter would otherwise call `reposition`
			// every frame, which several compositors react to by
			// dropping the popup grab.
			if let SurfaceKind::Popup( ref popup ) = ss.surface
			{
				if let ( Some( anchor_id ), Some( xdg_shell ) ) = ( spec.anchor_widget_id, xdg_shell_opt )
				{
					if let Some( anchor_rect ) = main_widget_rects.iter()
						.find( |w| w.id == Some( anchor_id ) )
						.map( |w| w.rect )
					{
						let to_logical = | r: Rect |
						{
							(
								( r.x      / parent_scale ).round() as i32,
								( r.y      / parent_scale ).round() as i32,
								( r.width  / parent_scale ).round().max( 1.0 ) as i32,
								( r.height / parent_scale ).round().max( 1.0 ) as i32,
							)
						};
						let new_q  = to_logical( anchor_rect );
						let moved  = ss.last_popup_anchor.map( |r| to_logical( r ) != new_q ).unwrap_or( true );
						if moved
						{
							if let Ok( positioner ) = XdgPositioner::new( xdg_shell )
							{
								let ( ax, ay, aw, ah ) = new_q;
								let ( spec_w, spec_h ) = resolved_size;
								let popup_w = if spec_w == 0 { aw } else { spec_w.max( 1 ) as i32 };
								let popup_h = spec_h.max( 1 ) as i32;
								positioner.set_size( popup_w, popup_h );
								positioner.set_anchor_rect( ax, ay, aw, ah );
								positioner.set_anchor( PositionerAnchor::Bottom );
								positioner.set_gravity( Gravity::Bottom );
								positioner.set_constraint_adjustment(
									ConstraintAdjustment::FlipY | ConstraintAdjustment::SlideX,
								);
								ss.popup_reposition_token = ss.popup_reposition_token.wrapping_add( 1 );
								popup.reposition( &positioner, ss.popup_reposition_token );
								ss.last_popup_anchor = Some( anchor_rect );
							}
						}
					}
				}
			}
			continue;
		}
		// `anchor_widget_id = Some(_)` → xdg-popup path; `None` →
		// wlr-layer-shell path.
		if let Some( anchor_id ) = spec.anchor_widget_id
		{
			let Some( xdg_shell ) = xdg_shell_opt else
			{
				eprintln!( "ltk: ignoring popup overlay {:?} — main surface is not an xdg-shell window", spec.id );
				continue;
			};
			let Some( ref parent ) = parent_xdg else
			{
				eprintln!( "ltk: ignoring popup overlay {:?} — no xdg parent surface", spec.id );
				continue;
			};
			let Some( anchor_rect ) = main_widget_rects.iter()
				.find( |w| w.id == Some( anchor_id ) )
				.map( |w| w.rect )
			else
			{
				eprintln!( "ltk: popup overlay {:?} could not find anchor widget id {:?}", spec.id, anchor_id );
				continue;
			};
			let positioner = match XdgPositioner::new( xdg_shell )
			{
				Ok( p )  => p,
				Err( e ) =>
				{
					eprintln!( "ltk: XdgPositioner::new failed for popup {:?}: {e}", spec.id );
					continue;
				}
			};
			// Convert the requested popup size and the anchor rect from
			// physical pixels (the layout coordinate space) to logical
			// pixels — the positioner expresses everything in window
			// geometry, which is logical. `size.0 == 0` is the
			// "match anchor width" convention (paralleling the
			// layer-shell `0 = fill` semantic): the popup is sized to
			// the trigger pill so combos / selects render flush with
			// the field they belong to.
			let ax = ( anchor_rect.x      / parent_scale ).round() as i32;
			let ay = ( anchor_rect.y      / parent_scale ).round() as i32;
			let aw = ( anchor_rect.width  / parent_scale ).round().max( 1.0 ) as i32;
			let ah = ( anchor_rect.height / parent_scale ).round().max( 1.0 ) as i32;
			let ( spec_w, spec_h ) = resolved_size;
			let popup_w = if spec_w == 0 { aw } else { spec_w.max( 1 ) as i32 };
			let popup_h = spec_h.max( 1 ) as i32;
			positioner.set_size( popup_w, popup_h );
			positioner.set_anchor_rect( ax, ay, aw, ah );
			positioner.set_anchor( PositionerAnchor::Bottom );
			positioner.set_gravity( Gravity::Bottom );
			positioner.set_constraint_adjustment(
				ConstraintAdjustment::FlipY | ConstraintAdjustment::SlideX,
			);
			// xdg_popup.grab must be issued before the first commit
			// (error 0 `invalid_grab` otherwise). `Popup::new` commits
			// internally, so the lower-level `from_surface` path is
			// the only one that lets the grab land in time.
			let surface = match Surface::new( cs, qh )
			{
				Ok( s )  => s,
				Err( e ) =>
				{
					eprintln!( "ltk: Surface::new failed for popup overlay {:?}: {e}", spec.id );
					continue;
				}
			};
			let popup = match Popup::from_surface( Some( parent ), &positioner, qh, surface, xdg_shell )
			{
				Ok( p )  => p,
				Err( e ) =>
				{
					eprintln!( "ltk: Popup::from_surface failed for overlay {:?}: {e}", spec.id );
					continue;
				}
			};
			if let Some( ref seat ) = grab_seat
			{
				popup.xdg_popup().grab( seat, grab_serial );
			}
			popup.wl_surface().commit();
			let mut ss = SurfaceState::<A::Message>::new( SurfaceKind::Popup( popup ), 0.0, String::new() );
			ss.last_requested_size = resolved_size;
			ss.last_popup_anchor   = Some( anchor_rect );
			overlays_m.insert( spec.id, ss );
			continue;
		}
		// wlr-layer-shell path.
		let Some( layer_shell ) = layer_shell_opt else
		{
			eprintln!( "ltk: ignoring layer-shell overlay {:?} — wlr-layer-shell not available", spec.id );
			continue;
		};
		let cfg = LayerConfig
		{
			layer:              spec.layer.to_wlr_layer(),
			exclusive_zone:     to_logical_zone( spec.exclusive_zone ),
			anchor:             spec.anchor,
			size:               to_logical_size( resolved_size ),
			keyboard_exclusive: spec.keyboard_exclusive,
			namespace:          "ltk-overlay",
		};
		let mut surface = SurfaceKind::Pending( cfg );
		if let Some( ref output ) = output_opt
		{
			surface.materialize( cs, layer_shell, qh, output );
		}
		let mut ss = SurfaceState::<A::Message>::new( surface, 0.0, String::new() );
		// Inherit the parent's scale so the first configure allocates a
		// HiDPI buffer and lays out at the right size from frame one,
		// instead of rendering at scale 1 until `scale_factor_changed`
		// lands a frame or two later.
		ss.scale_factor        = parent_scale_i;
		ss.last_requested_size = resolved_size;
		ss.last_requested_zone = to_logical_zone( spec.exclusive_zone );
		ss.layer_anchor        = Some( spec.anchor );
		overlays_m.insert( spec.id, ss );
	}
}
