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

use smithay_client_toolkit::
{
	compositor::CompositorState,
	output::OutputState,
	registry::RegistryState,
	seat::SeatState,
	shell::{ wlr_layer::LayerShell, xdg::XdgShell },
	shm::Shm,
	session_lock::SessionLock,
	subcompositor::SubcompositorState,
};
use smithay_client_toolkit::reexports::client::
{
	protocol::
	{
		wl_keyboard::WlKeyboard,
		wl_output::WlOutput,
		wl_pointer::WlPointer,
		wl_surface::WlSurface,
		wl_touch::WlTouch,
	},
	QueueHandle,
};
use wayland_protocols::wp::text_input::zv3::client::
{
	zwp_text_input_manager_v3::ZwpTextInputManagerV3,
	zwp_text_input_v3::ZwpTextInputV3,
};
use std::collections::HashMap;
use std::sync::Arc;

use crate::app::{ App, OverlayId, SubsurfaceId };
use crate::egl_context::EglContext;
use crate::types::Point;

use super::repeat::{ ButtonRepeatState, KeyRepeatState };
use super::subsurface::SubsurfaceSlot;
use super::surface::{ SurfaceFocus, SurfaceState };
use super::tooltip::{ TooltipPending, TooltipVisible };

pub struct AppData<A: App>
{
	pub app:                A,
	pub registry_state:     RegistryState,
	pub seat_state:         SeatState,
	pub output_state:       OutputState,
	pub compositor_state:   CompositorState,
	/// `wl_subcompositor` binding for [`crate::app::App::subsurfaces`].
	/// `None` when the compositor does not advertise the protocol.
	pub subcompositor:      Option<SubcompositorState>,
	pub shm:                Shm,
	pub session_lock:       Option<SessionLock>,
	/// Process-wide EGL context (display + GLES context). `None` when EGL
	/// failed to initialise or `LTK_FORCE_SOFTWARE=1` — every surface then
	/// falls back to the SHM path.
	pub egl_context:        Option<Arc<EglContext>>,
	#[ allow( dead_code ) ]
	pub xdg_shell:          Option<XdgShell>,
	/// Shared layer-shell binding used for the main surface and every
	/// overlay. `None` when the compositor does not advertise the protocol.
	pub layer_shell:        Option<LayerShell>,
	pub keyboard:           Option<WlKeyboard>,
	pub pointer:            Option<WlPointer>,
	pub touch:              Option<WlTouch>,
	pub pointer_pos:        Point,
	/// Process-wide cursor-shape manager (`wp_cursor_shape_v1`).
	/// `None` when the compositor does not advertise the protocol —
	/// the runtime then leaves cursor shape to the compositor's
	/// defaults.
	pub cursor_shape_manager: Option<smithay_client_toolkit::seat::pointer::cursor_shape::CursorShapeManager>,
	/// Per-pointer cursor-shape device, populated when the compositor
	/// supports cursor-shape AND the seat has a pointer capability.
	pub cursor_shape_device:  Option<smithay_client_toolkit::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>,
	/// Last `Enter` serial seen on a pointer event. Required by
	/// `wp_cursor_shape_device_v1::set_shape` per spec — the
	/// compositor tags every cursor change with the entry serial.
	pub last_pointer_enter_serial: u32,
	/// The cursor shape currently active on the pointer. `None`
	/// means "we have not pushed any shape since the last
	/// `wl_pointer.enter`" — the compositor is showing whatever the
	/// previous client requested, so the next `dispatch_cursor_shape`
	/// must send unconditionally to claim the cursor for our
	/// surface. `Some(s)` lets the dispatch short-circuit when the
	/// target equals what we already sent.
	pub current_cursor_shape: Option<crate::types::CursorShape>,
	pub text_input_manager: Option<ZwpTextInputManagerV3>,
	pub text_input:         Option<ZwpTextInputV3>,
	/// Whether the currently focused text field is `secure` (password).
	/// Drives the text-input-v3 content type.
	pub text_input_secure:  bool,
	/// `xdg-activation-v1`. Present when the compositor advertises the
	/// global. Used on first configure to honour an incoming
	/// `XDG_ACTIVATION_TOKEN` (a launcher that spawned us wants the
	/// main surface raised to focus) and exposed via
	/// [`App::request_activation_token`] for outbound requests.
	pub activation_state:
		Option<smithay_client_toolkit::activation::ActivationState>,
	/// Honour the activation token exactly once — after the first
	/// successful configure of the main surface. Subsequent configures
	/// (resizes, scale changes) must not re-activate.
	pub activation_token_pending: Option<String>,

	/// `wl_data_device_manager` binding. `None` when the compositor
	/// does not advertise the global, in which case copy / paste stays
	/// process-local and inbound selections from other clients are
	/// invisible.
	pub data_device_manager:
		Option<smithay_client_toolkit::data_device_manager::DataDeviceManagerState>,
	/// Per-seat `wl_data_device` handle, created the first time a seat
	/// with a keyboard or pointer capability appears. Required for
	/// `set_selection` and to receive inbound `selection` events.
	pub data_device:
		Option<smithay_client_toolkit::data_device_manager::data_device::DataDevice>,
	/// Currently-published outbound selection source. Held alive
	/// across [`DataSourceHandler::send_request`] so the cached
	/// clipboard text can be re-served on each paste from the peer.
	pub clipboard_source:
		Option<smithay_client_toolkit::data_device_manager::data_source::CopyPasteSource>,
	/// Sender half of the cross-thread channel used to ferry inbound
	/// selection bytes (a worker thread drains the read pipe).
	pub clipboard_inbox_tx: std::sync::mpsc::Sender<String>,
	/// Receiver half — drained once per run loop iteration.
	pub clipboard_inbox_rx: std::sync::mpsc::Receiver<String>,

	/// Cross-application drag-and-drop: best mime negotiated on
	/// `enter` and the last pointer position (in logical surface
	/// coords). Cleared on `leave` and after `drop_performed`.
	pub drop_position: Option<( f64, f64 )>,
	pub drop_mime:     Option<String>,
	pub drop_inbox_tx: std::sync::mpsc::Sender<super::data_device::DropPayload>,
	pub drop_inbox_rx: std::sync::mpsc::Receiver<super::data_device::DropPayload>,

	/// AccessKit / AT-SPI2 adapter. `None` when the platform adapter
	/// could not be created (no AT-SPI2 daemon on the session bus,
	/// missing system libraries at runtime, headless CI). The
	/// runtime carries on without an accessibility tree in that
	/// case; nothing else in the pipeline reads this field except
	/// the per-frame tree update and the per-iteration action
	/// inbox drain.
	pub a11y: Option<crate::a11y::A11yState>,
	/// Client-side handle to `ext-foreign-toplevel-list-v1`. Holds the
	/// global proxy plus the live list of open toplevels; SCTK fans
	/// events through this object into our `ForeignToplevelListHandler`
	/// impl in `handlers.rs`, which then routes them to
	/// `App::on_toplevel_event`. Present on every session, even when
	/// the compositor does not advertise the global — internally the
	/// list holds a `GlobalProxy` that simply yields no toplevels in
	/// that case.
	pub foreign_toplevel_list:
		smithay_client_toolkit::foreign_toplevel_list::ForeignToplevelList,
	pub shift_pressed:      bool,
	pub ctrl_pressed:       bool,
	/// Calloop handle for inserting timers / channels. Used by the
	/// key-repeat machinery and any future feature that needs to
	/// schedule work on the run loop without going through the message
	/// queue.
	pub loop_handle:        calloop::LoopHandle<'static, Self>,
	/// Repeat rate (events per second) advertised by the compositor in
	/// `wl_keyboard.repeat_info`. `0` means "the compositor disabled
	/// repeat" — we honour that and never start a repeat timer.
	pub compositor_repeat_rate:  u32,
	/// Initial delay (ms) before the first repeat fires, advertised by
	/// the compositor. `0` means the compositor disabled repeat.
	pub compositor_repeat_delay: u32,
	/// Currently-active key repeat. `Some` between the press of a
	/// repeating key and either its release, the keyboard losing focus
	/// or another key being pressed.
	pub key_repeat:         Option<KeyRepeatState>,
	/// Currently-active button-press repeat. `Some` between the
	/// press of a repeating-button and either its release, the
	/// pointer leaving the surface, a gesture cancel, focus loss or
	/// long-press promotion.
	pub button_repeat:      Option<ButtonRepeatState>,
	/// Clipboard buffer. Copy / Cut store the selected text here;
	/// Paste retrieves from here. Synchronised with the Wayland
	/// selection through `wl_data_device_manager` when the compositor
	/// advertises the global: outbound copies publish a
	/// `CopyPasteSource`, and inbound selections from other clients
	/// land here through a worker thread (see
	/// [`super::data_device`]).
	pub clipboard:          String,
	/// Timestamp of the previous press (pointer or touch). Combined
	/// with [`Self::last_press_pos`] it lets the press handler
	/// detect a double-click on a TextEdit and turn it into a
	/// word-select.
	pub last_press_time:    Option<std::time::Instant>,
	/// Position of the previous press. See [`Self::last_press_time`].
	pub last_press_pos:     Option<Point>,
	pub debug_layout:       bool,
	pub pending_msgs:       Vec<A::Message>,
	/// Initial cursor positions queued when a long-press fires, to be flushed
	/// to [`App::on_drag_move`] right after the paired long-press message is
	/// processed. This gives the drag ghost a valid starting position even
	/// before the user's finger / cursor moves — useful on mouse where the
	/// pointer might sit perfectly still between press and drag.
	pub pending_drag_inits: Vec<Point>,
	pub tooltip_pending: Option<TooltipPending>,
	pub tooltip_visible: Option<TooltipVisible>,
	pub qh:                 QueueHandle<Self>,
	/// Last pointer serial (needed for interactive move).
	pub last_pointer_serial: u32,
	/// Last serial from any input event. Required by `xdg_popup.grab`,
	/// which only honours serials from recent input events on the same
	/// seat.
	pub last_input_serial:   u32,
	/// Set to true when the app has accepted a close request.
	pub exit_requested:      bool,
	/// First-configure latch for `App::start_fullscreen`.
	pub pending_fullscreen:  bool,
	/// First-configure latch: clears `max_size` after the compositor
	/// has honoured the pinned initial size.
	pub pending_size_hint_unpin: bool,
	/// Main application surface.
	pub main:                SurfaceState<A::Message>,
	/// Auxiliary layer-shell surfaces keyed by their stable [`OverlayId`].
	/// Populated and reconciled by the run loop from [`App::overlays`]. Always
	/// empty until overlay diffing is wired up.
	pub overlays:            HashMap<OverlayId, SurfaceState<A::Message>>,
	/// Surfaces covering every output beyond the first, paired with the
	/// output each one belongs to: session-lock surfaces while locked (the
	/// compositor blanks any output without one), background layer surfaces
	/// for a [`crate::Layer::Background`] shell (the wallpaper reaches
	/// extended outputs). These render [`App::secondary_view`] and take no
	/// input (the input handlers find no focus for them, and keyboard falls
	/// through to `Main`).
	pub secondaries:         Vec<( WlOutput, SurfaceState<A::Message> )>,
	/// Output the main surface was created on, so a later `new_output` for
	/// the same output does not add a duplicate secondary surface.
	pub main_output:         Option<WlOutput>,
	/// Input-transparent child surfaces keyed by their stable
	/// [`SubsurfaceId`]. Reconciled each frame from [`App::subsurfaces`].
	pub subsurfaces:         HashMap<SubsurfaceId, SubsurfaceSlot>,
	/// Shared GLES canvas reused across every subsurface raster. Held here
	/// (not per-slot) so a subsurface that is dropped and re-created — e.g. a
	/// lazily-emitted sliding panel — does not recompile the ~14 shader
	/// programs (hundreds of ms on a mobile GPU) every time it reappears.
	pub subsurface_gles_canvas: Option<crate::render::Canvas>,
	/// Surface currently receiving pointer events (updated on each pointer
	/// event via its `surface` field).
	pub pointer_focus:       SurfaceFocus,
	/// Surface currently holding keyboard focus (updated on keyboard
	/// enter/leave).
	pub keyboard_focus:      SurfaceFocus,
	/// Per-touch-id tracking of which surface each active touch belongs to
	/// (populated on `down`, cleared on `up`/`cancel`).
	pub touch_focus:         HashMap<i32, SurfaceFocus>,
	/// Cached widget tree returned by [`App::view`]. Populated lazily by the
	/// run loop just before drawing when `view_dirty` is set, then re-used by
	/// every subsequent partial / interaction-only redraw until invalidated.
	/// Dropping the rebuild on idle frames is a big win for shells that sit
	/// idle most of the time.
	pub cached_view:         Option<crate::widget::Element<A::Message>>,
	/// Cached overlay-spec list returned by [`App::overlays`]. Same lifecycle
	/// as `cached_view` but driven by `overlays_dirty`.
	pub cached_overlays:     Option<Vec<crate::app::OverlaySpec<A::Message>>>,
	/// `true` when `cached_view` is stale and must be rebuilt before the next
	/// draw. Set on startup, by every `InvalidationScope` touching `Main`, and
	/// every frame `App::is_animating` returns `true`.
	pub view_dirty:          bool,
	/// Counterpart of `view_dirty` for `cached_overlays`.
	pub overlays_dirty:      bool,
	/// Set after the first commit of a rendered buffer on the main surface.
	/// Used to drive `App::on_first_frame_committed` exactly once.
	pub first_frame_committed: bool,
	/// Focus request deferred because the target widget was absent from
	/// `widget_rects` (e.g. read_only, not yet rebuilt). Retried next draw.
	pub focus_retry: Option<crate::types::WidgetId>,
	/// Runtime performance guardrails (stuck-animation / poll-interval
	/// diagnostics and the software animation-rate cap). See [`super::perf`].
	pub perf: super::perf::PerfState,
}

impl<A: App> AppData<A>
{
	/// Find which [`SurfaceFocus`] owns the given `WlSurface`, or `None` if it
	/// does not correspond to any tracked surface. Safe even when the main
	/// surface is still `Pending` (returns `None`).
	pub( crate ) fn focus_for_surface(
		&self,
		wl: &WlSurface,
	) -> Option<SurfaceFocus>
	{
		if self.main.surface.try_wl_surface() == Some( wl )
		{
			return Some( SurfaceFocus::Main );
		}
		for ( id, ss ) in self.overlays.iter()
		{
			if ss.surface.try_wl_surface() == Some( wl )
			{
				return Some( SurfaceFocus::Overlay( *id ) );
			}
		}
		None
	}

	/// Whether `wl` is one of the secondary per-output surfaces. Pointer and
	/// touch events landing on them must be dropped rather than falling back
	/// to `Main` — their coordinates would hit-test the main surface's
	/// widgets. Keyboard keeps the `Main` fallback so typing works wherever
	/// the compositor puts the focus.
	pub( crate ) fn is_secondary_surface( &self, wl: &WlSurface ) -> bool
	{
		self.secondaries.iter().any( |( _, ss )| ss.surface.try_wl_surface() == Some( wl ) )
	}

	/// Borrow the [`SurfaceState`] identified by `focus`. Panics if `focus`
	/// refers to an overlay that is not currently registered — callers must
	/// only pass focus values obtained from [`focus_for_surface`] or from the
	/// per-device focus fields, which the run loop keeps in sync.
	#[ allow( dead_code ) ]
	pub( crate ) fn surface( &self, focus: SurfaceFocus ) -> &SurfaceState<A::Message>
	{
		match focus
		{
			SurfaceFocus::Main         => &self.main,
			SurfaceFocus::Overlay( id ) => self.overlays.get( &id )
				.expect( "surface(): overlay not registered" ),
		}
	}

	/// Non-panicking variant of [`surface`]. Returns `None` when `focus`
	/// refers to an overlay that has already been removed — callers on
	/// async dispatch paths (IME Done, tooltip arm) must use this.
	pub( crate ) fn try_surface( &self, focus: SurfaceFocus ) -> Option<&SurfaceState<A::Message>>
	{
		match focus
		{
			SurfaceFocus::Main         => Some( &self.main ),
			SurfaceFocus::Overlay( id ) => self.overlays.get( &id ),
		}
	}

	/// Mutable counterpart of [`surface`].
	#[ allow( dead_code ) ]
	pub( crate ) fn surface_mut( &mut self, focus: SurfaceFocus ) -> &mut SurfaceState<A::Message>
	{
		match focus
		{
			SurfaceFocus::Main         => &mut self.main,
			SurfaceFocus::Overlay( id ) => self.overlays.get_mut( &id )
				.expect( "surface_mut(): overlay not registered" ),
		}
	}

	/// Non-panicking variant of [`surface_mut`].
	pub( crate ) fn try_surface_mut( &mut self, focus: SurfaceFocus ) -> Option<&mut SurfaceState<A::Message>>
	{
		match focus
		{
			SurfaceFocus::Main         => Some( &mut self.main ),
			SurfaceFocus::Overlay( id ) => self.overlays.get_mut( &id ),
		}
	}

	/// Synchronous overlay teardown: removes the overlay from the map
	/// and rewrites every per-device focus that pointed at it so the
	/// next event in the same dispatch can no longer land on a freed
	/// surface. Used by the compositor-driven destruction paths
	/// (`PopupHandler::done`, `LayerShellHandler::closed`) where
	/// waiting for the next `reconcile_overlays` would leave a window
	/// in which `surface()` / `surface_mut()` panic. Migrates an
	/// in-flight long-press drag to the main surface for the same
	/// reason `reconcile_overlays` does.
	pub( crate ) fn discard_overlay( &mut self, id: crate::app::OverlayId )
	{
		if let Some( ss ) = self.overlays.remove( &id )
		{
			if ss.gesture.long_press_fired
			{
				self.main.gesture.long_press_fired  = true;
				self.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 self.main.primary_touch_id.is_none()
				{
					self.main.primary_touch_id = ss.primary_touch_id;
				}
			}
		}
		if let SurfaceFocus::Overlay( fid ) = self.pointer_focus
		{
			if fid == id { self.pointer_focus = SurfaceFocus::Main; }
		}
		if let SurfaceFocus::Overlay( fid ) = self.keyboard_focus
		{
			if fid == id { self.keyboard_focus = SurfaceFocus::Main; }
		}
		for f in self.touch_focus.values_mut()
		{
			if let SurfaceFocus::Overlay( fid ) = *f
			{
				if fid == id { *f = SurfaceFocus::Main; }
			}
		}
		if let Some( pending ) = self.tooltip_pending.as_ref()
		{
			if pending.focus == SurfaceFocus::Overlay( id ) { self.tooltip_pending = None; }
		}
		if let Some( visible ) = self.tooltip_visible.as_ref()
		{
			if visible.focus == SurfaceFocus::Overlay( id )
			{
				self.tooltip_visible = None;
				self.overlays_dirty  = true;
			}
		}
	}

	// Configure the main surface size, (re)allocate its rendering target, and
	// request a redraw. Routes to the GPU or SHM path inside `SurfaceState`
	// according to whether `self.egl_context` is available.
	pub( crate ) fn on_configure( &mut self, w: u32, h: u32 )
	{
		self.main.on_configure( &self.shm, self.egl_context.as_ref(), w, h );
		if let Some( ref mut a ) = self.a11y
		{
			let sf = self.main.scale_factor.max( 1 ) as f64;
			a.set_window_bounds( ( w as f64 ) * sf, ( h as f64 ) * sf );
			a.set_window_focus( true );
		}
		// First-configure latch: honour an incoming
		// `XDG_ACTIVATION_TOKEN` exactly once, after the surface has
		// been mapped (`xdg_activation_v1.activate` is only meaningful
		// against a configured surface). Compositors that don't
		// advertise the global leave `activation_state` as `None` and
		// the call drops the token silently.
		if let Some( token ) = self.activation_token_pending.take()
		{
			if let ( Some( ref activation ), Some( wl ) ) =
				( self.activation_state.as_ref(), self.main.surface.try_wl_surface() )
			{
				activation.activate::<Self>( &wl, token );
			}
		}
		// `on_resize` is documented to deliver **physical** pixels, matching the
		// coordinate space that the layout passes and the pointer/touch
		// callbacks (`on_drag_move`, `on_drop`) work in. Wayland's
		// configure.new_size is surface-local (logical), so multiply by the
		// current buffer scale before handing the dimensions to the app.
		let sf = self.main.scale_factor.max( 1 ) as u32;
		crate::types::set_viewport_size( w * sf, h * sf );
		self.app.on_scale_changed( sf );
		self.app.on_resize( w * sf, h * sf );
		// `on_resize` may flip app-state that the view depends on (apps that
		// branch on the new dimensions, layout caches keyed by size, …), so
		// drop the cached tree to force a rebuild on the next draw.
		self.dirty_caches();
	}

	/// Mark both view caches stale after a direct app-state mutation that
	/// doesn't go through [`App::update`] — swipe progress callbacks, text-
	/// input focus changes, configure-driven resize. Per-message updates use
	/// the run loop's `apply_invalidation` path instead so that
	/// [`App::invalidate_after`] can scope the rebuild.
	pub( crate ) fn dirty_caches( &mut self )
	{
		self.view_dirty     = true;
		self.overlays_dirty = true;
	}

	/// Smallest remaining time until a pending long-press deadline fires.
	///
	/// Used by the run loop to bound `event_loop.dispatch()` so that a
	/// stationary press wakes us up at the right moment even when no
	/// Wayland events arrive. `Some(Duration::ZERO)` means the deadline
	/// has already elapsed; `None` means nothing is pending.
	pub( crate ) fn next_long_press_wakeup( &self ) -> Option<std::time::Duration>
	{
		let dur = self.app.long_press_duration();
		let now = std::time::Instant::now();
		let mut soonest: Option<std::time::Duration> = None;
		let mut consider = |start: Option<std::time::Instant>|
		{
			if let Some( s ) = start
			{
				let deadline  = s + dur;
				let remaining = deadline.saturating_duration_since( now );
				soonest = Some( match soonest
				{
					Some( cur ) => cur.min( remaining ),
					None        => remaining,
				} );
			}
		};
		consider( self.main.gesture.long_press_start );
		for ss in self.overlays.values()
		{
			consider( ss.gesture.long_press_start );
		}
		soonest
	}
}
