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

use smithay_client_toolkit::
{
	compositor::CompositorState,
	shell::
	{
		WaylandSurface,
		wlr_layer::{ KeyboardInteractivity, Layer, LayerShell, LayerSurface },
		xdg::{ popup::Popup, window::Window },
	},
	shm::{ Shm, slot::SlotPool },
};
use smithay_client_toolkit::reexports::client::
{
	protocol::wl_surface::WlSurface,
	QueueHandle,
};
use smithay_client_toolkit::session_lock::SessionLockSurface;
use std::collections::HashMap;
use std::sync::Arc;

use super::app_data::AppData;
use crate::app::OverlayId;
use crate::egl_context::{ EglContext, EglSurface };
use crate::input::GestureState;
use crate::render::Canvas;
use crate::types::{ Point, Rect, WidgetId };
use crate::widget::LaidOutWidget;

/// Identifies which surface an event, focus, or draw call belongs to.
///
/// `Main` refers to the application's main surface (xdg window or layer
/// shell). `Overlay( id )` refers to an auxiliary layer-shell surface created
/// from an entry in [`crate::app::App::overlays`].
#[ derive( Debug, Clone, Copy, PartialEq, Eq, Hash ) ]
pub( crate ) enum SurfaceFocus
{
	Main,
	#[ allow( dead_code ) ]
	Overlay( OverlayId ),
}

/// Configuration for a layer-shell surface, used both for the main surface
/// (when the app uses [`crate::app::ShellMode::Layer`]) and for each overlay
/// returned by [`crate::app::App::overlays`].
#[ derive( Clone ) ]
pub( crate ) struct LayerConfig
{
	pub layer:              Layer,
	pub exclusive_zone:     i32,
	pub anchor:             crate::app::Anchor,
	pub size:               ( u32, u32 ),
	pub keyboard_exclusive: bool,
	/// Wayland surface role namespace, sent to the compositor for debugging.
	pub namespace:          &'static str,
}

pub( crate ) enum SurfaceKind
{
	/// Layer shell surface, fully created and committed.
	Layer( LayerSurface ),
	/// XDG window fallback (no layer shell compositor support).
	Window( Window ),
	/// Layer shell is available but we are waiting for an output to be
	/// advertised before creating the Wayland surface.
	Pending( LayerConfig ),
	/// XDG popup, child of the main window. Used for combo dropdowns,
	/// context menus, tooltips. The compositor positions it relative
	/// to an anchor rect specified at creation time and may flip it
	/// (drop-up vs drop-down) when constrained.
	Popup( Popup ),
	/// `ext-session-lock-v1` lock surface, created in `SessionLockHandler::locked`.
	Lock( SessionLockSurface ),
	/// Session lock requested; waiting for the compositor's `locked` event
	/// before the lock surface exists.
	PendingLock,
}

impl SurfaceKind
{
	pub( crate ) fn wl_surface( &self ) -> &WlSurface
	{
		match self
		{
			SurfaceKind::Layer( l )  => l.wl_surface(),
			SurfaceKind::Window( w ) => w.wl_surface(),
			SurfaceKind::Popup( p )  => p.wl_surface(),
			SurfaceKind::Lock( s )   => s.wl_surface(),
			// Unreachable: draw_frame is only called when configured == true,
			// which only becomes true after on_configure, which requires a real surface.
			SurfaceKind::Pending( .. ) | SurfaceKind::PendingLock => unreachable!( "surface not yet created" ),
		}
	}

	/// Same as [`wl_surface`] but returns `None` for a [`SurfaceKind::Pending`]
	/// surface instead of panicking. Used by event dispatch to look up which
	/// [`SurfaceState`] an incoming Wayland event belongs to without risk of
	/// crashing during startup.
	pub( crate ) fn try_wl_surface( &self ) -> Option<&WlSurface>
	{
		match self
		{
			SurfaceKind::Layer( l )    => Some( l.wl_surface() ),
			SurfaceKind::Window( w )   => Some( w.wl_surface() ),
			SurfaceKind::Popup( p )    => Some( p.wl_surface() ),
			SurfaceKind::Lock( s )     => Some( s.wl_surface() ),
			SurfaceKind::Pending( .. ) | SurfaceKind::PendingLock => None,
		}
	}

	/// Create the actual layer surface on `output` using `layer_shell`. Returns
	/// true if the surface was created (i.e., was `Pending` before).
	pub( crate ) fn materialize(
		&mut self,
		compositor:  &CompositorState,
		layer_shell: &LayerShell,
		qh:          &QueueHandle<AppData<impl crate::app::App>>,
		output:      &smithay_client_toolkit::reexports::client::protocol::wl_output::WlOutput,
	) -> bool
	{
		if let SurfaceKind::Pending( ref cfg ) = *self
		{
			let surface       = compositor.create_surface( qh );
			let layer_surface = layer_shell.create_layer_surface(
				qh,
				surface,
				cfg.layer,
				Some( cfg.namespace ),
				Some( output ),
			);
			layer_surface.set_exclusive_zone( cfg.exclusive_zone );
			layer_surface.set_anchor( cfg.anchor.to_wlr_anchor() );
			let interactivity = if cfg.keyboard_exclusive
			{
				KeyboardInteractivity::Exclusive
			} else {
				KeyboardInteractivity::OnDemand
			};
			layer_surface.set_keyboard_interactivity( interactivity );
			layer_surface.set_size( cfg.size.0, cfg.size.1 );
			layer_surface.commit();
			*self = SurfaceKind::Layer( layer_surface );
			true
		} else {
			false
		}
	}
}

/// The subset of per-surface state the draw pass owns and threads through
/// [`crate::draw::DrawCtx`]: the laid-out widget rects it produces, the scroll /
/// text-edit / a11y state carried across frames, and the previous frame's
/// interaction indices for damage tracking. Grouped into its own struct so the
/// draw helpers can borrow it as one unit, disjoint from `canvas` / `pool`.
pub( crate ) struct FrameState<Msg: Clone>
{
	pub widget_rects:       Vec<LaidOutWidget<Msg>>,
	pub cursor_state:       HashMap<usize, usize>,
	/// Selection anchor (byte offset). When equal to the cursor, there is no
	/// active selection. Mutated jointly with `cursor_state`: plain arrow keys
	/// collapse the selection by setting anchor = cursor; Shift+arrow extends by
	/// leaving anchor put while moving cursor; pointer drags set both ends.
	pub selection_anchor:   HashMap<usize, usize>,
	pub accessible_extras:  Vec<crate::a11y::tree::AccessibleExtra>,
	/// Per-scroll-viewport offset, indexed by `flat_idx` of the `Scroll` widget.
	/// Tuple is `(x, y)` in physical pixels: x is applied only when the widget's
	/// axis allows horizontal scroll (and stays at `0` otherwise), y mirrors the
	/// historic single-f32 behaviour for vertical scrolls.
	pub scroll_offsets:     HashMap<usize, ( f32, f32 )>,
	pub scroll_rects:       Vec<( Rect, usize, crate::widget::scroll::ScrollAxis )>,
	pub scroll_canvases:    HashMap<usize, Canvas>,
	/// Per-scroll list of `(flat_idx, content_y, height)` entries for every
	/// interactive item the scroll's child laid out, in document order. Includes
	/// items currently scrolled off-screen so keyboard arrow handlers can step
	/// `hovered_idx` item-by-item without regenerating the layout pass. Y is in
	/// pre-translation, pre-offset content coordinates.
	pub scroll_navigable_items: HashMap<usize, Vec<( usize, f32, f32 )>>,
	/// Previous frame interaction state for damage tracking.
	pub prev_focused:       Option<usize>,
	pub prev_hovered:       Option<usize>,
	pub prev_pressed:       Option<usize>,
}

impl<Msg: Clone> FrameState<Msg>
{
	pub( crate ) fn new() -> Self
	{
		Self
		{
			widget_rects:       Vec::new(),
			cursor_state:       HashMap::new(),
			selection_anchor:   HashMap::new(),
			accessible_extras:  Vec::new(),
			scroll_offsets:     HashMap::new(),
			scroll_rects:       Vec::new(),
			scroll_canvases:    HashMap::new(),
			scroll_navigable_items: HashMap::new(),
			prev_focused:       None,
			prev_hovered:       None,
			prev_pressed:       None,
		}
	}
}

/// Per-surface render + interaction state.
///
/// Each Wayland surface managed by the event loop (main surface and any
/// active overlays) owns an instance of this struct, so multiple surfaces
/// can coexist without interfering.
pub( crate ) struct SurfaceState<Msg: Clone>
{
	pub pool:               Option<SlotPool>,
	/// EGL window surface for the GPU path. Populated on the first configure
	/// when an [`EglContext`] is available; mutually exclusive with `pool`
	/// (the path is locked after the first configure).
	pub egl_surface:        Option<EglSurface>,
	pub surface:            SurfaceKind,
	pub canvas:             Option<Canvas>,
	pub width:              u32,
	pub height:             u32,
	pub configured:         bool,
	pub needs_redraw:       bool,
	/// Set to `true` when the app's view may have changed in a way that rects
	/// don't capture (text content, progress value, slider thumb position, etc).
	/// Interaction-only transitions (hover in/out, pressed in/out) leave this
	/// `false` so the draw path can take the partial-redraw fast path.
	/// Always reset to `false` at the end of a successful draw.
	pub content_dirty:      bool,
	pub last_draw:          std::time::Instant,
	pub focused_idx:        Option<usize>,
	pub focused_id:         Option<WidgetId>,
	pub hovered_idx:        Option<usize>,
	pub pending_text_values: HashMap<usize, String>,
	/// Per-frame draw state: laid-out rects, carried scroll / text-edit / a11y
	/// state, and the previous frame's interaction indices. Grouped so the draw
	/// helpers can borrow it as one unit, disjoint from `canvas` / `pool`.
	pub frame:              FrameState<Msg>,
	/// Built-in Copy / Cut / Paste context menu when shown on this
	/// surface. `None` means no menu is up.
	pub context_menu:           Option<super::context_menu::ContextMenu>,
	/// Per-surface gesture state machine. Owns the press →
	/// (long-press? / drag? / scroll? / swipe?) → release lifecycle,
	/// driven by both pointer and touch handlers. The input layer
	/// (`src/input/`) calls the lifecycle methods; the long-press
	/// deadline poller reaches into `gesture.long_press_*` directly.
	pub gesture:            GestureState<Msg>,
	/// `wl_touch.id` of the finger currently driving the single-slot
	/// gesture machine. `None` when no finger is down on this surface.
	/// Subsequent fingers landing while this is `Some` route through
	/// the auxiliary multi-touch path ([`App::on_touch_down`] etc.)
	/// instead of being absorbed into the primary slot.
	pub primary_touch_id:   Option<i32>,
	/// Auxiliary touch slots: every non-primary finger's last known
	/// position, keyed by `wl_touch.id`. Used by the touch handler to
	/// generate `on_touch_up` with the correct release point (Wayland
	/// `wl_touch.up` does not carry a position) and to keep state
	/// across `motion` events between dispatchers.
	pub touch_slots:        HashMap<i32, crate::types::Point>,
	/// Height of the client-side title bar (0 for layer-shell surfaces).
	pub titlebar_height:    f32,
	/// Title bar height before decoration negotiation; restored when the
	/// compositor hands decorations back to the client.
	pub titlebar_base:      f32,
	/// Title text shown in the client-side title bar.
	pub titlebar_title:     String,
	/// Rect of the close button in the title bar (for hit testing).
	pub titlebar_close_rect: Rect,
	/// Compositor scale factor (integer, e.g. 1 or 2). Used to create
	/// high-DPI buffers and set `dpi_scale` on the canvas.
	pub scale_factor:        i32,
	/// Last size requested from the compositor via `layer_surface.set_size`.
	/// Compared against the app's current [`crate::app::OverlaySpec::size`]
	/// each reconcile pass so that overlays can resize after creation (used
	/// for slide-down / grow animations). `(0, 0)` for non-layer surfaces
	/// and for layer surfaces before their first configure.
	pub last_requested_size: ( u32, u32 ),
	/// Logical exclusive zone last sent with `set_exclusive_zone`. Tracked
	/// alongside the size because a zone that stays at its creation-time
	/// value while the surface keeps resizing leaves the compositor
	/// reserving a band that no longer matches what the overlay paints.
	pub last_requested_zone: i32,
	pub layer_anchor:        Option<crate::app::Anchor>,
	/// Anchor rect the xdg-popup positioner was last configured with.
	/// `None` for non-popup surfaces.
	pub last_popup_anchor:   Option<Rect>,
	/// Monotonic counter for `xdg_popup.reposition` tokens.
	pub popup_reposition_token: u32,
	/// `true` after the surface has been committed and we are waiting for the
	/// matching `wl_surface.frame` callback to fire. Drawing is gated on this
	/// being `false`: even if `needs_redraw` flips back to `true` we sit on
	/// the next frame until the compositor signals it's time to commit again,
	/// so the per-surface pacing follows the compositor (and the display
	/// refresh / VRR / "screen is off") instead of a fixed-period timer.
	pub frame_pending: bool,
}

impl<Msg: Clone> SurfaceState<Msg>
{
	pub( crate ) fn new( surface: SurfaceKind, titlebar_height: f32, titlebar_title: String ) -> Self
	{
		Self {
			pool:                None,
			egl_surface:         None,
			surface,
			canvas:              None,
			width:               0,
			height:              0,
			configured:          false,
			needs_redraw:        false,
			content_dirty:       true,
			last_draw:           std::time::Instant::now() - std::time::Duration::from_millis( 100 ),
			focused_idx:         None,
			focused_id:          None,
			hovered_idx:         None,
			pending_text_values: HashMap::new(),
			frame:               FrameState::new(),
			primary_touch_id:    None,
			touch_slots:         HashMap::new(),
			context_menu:        None,
			gesture:             GestureState::new(),
			titlebar_height,
			titlebar_base:       titlebar_height,
			titlebar_title,
			titlebar_close_rect: Rect::default(),
			scale_factor:        1,
			last_requested_size: ( 0, 0 ),
			last_requested_zone: 0,
			layer_anchor:        None,
			last_popup_anchor:   None,
			popup_reposition_token: 0,
			frame_pending:       false,
		}
	}

	/// Convert a logical (surface-local) position to physical pixel coordinates.
	pub( crate ) fn to_physical( &self, x: f64, y: f64 ) -> Point
	{
		let sf = self.scale_factor.max( 1 ) as f32;
		Point { x: x as f32 * sf, y: y as f32 * sf }
	}

	/// Surface width in physical pixels (logical × buffer scale). Matches the
	/// coordinate space used by pointer/touch callbacks and layout, so swipe
	/// thresholds can be compared against physical `dx` / `start.x` without
	/// unit mixing.
	pub( crate ) fn physical_width( &self ) -> u32
	{
		self.width * self.scale_factor.max( 1 ) as u32
	}

	/// Surface height in physical pixels. See [`Self::physical_width`].
	pub( crate ) fn physical_height( &self ) -> u32
	{
		self.height * self.scale_factor.max( 1 ) as u32
	}

	/// Configure the surface with a new logical size. Picks the rendering path
	/// on the very first configure (GPU when `egl_context` is `Some` and the
	/// EGL window can be created, SHM otherwise) and just resizes the existing
	/// path on subsequent configures. The two paths are mutually exclusive
	/// (`pool.is_some()` ⇔ SHM, `egl_surface.is_some()` ⇔ GPU) and locked
	/// after the first configure — switching backends mid-life would require
	/// tearing down the canvas as well.
	pub( crate ) fn on_configure(
		&mut self,
		shm:         &Shm,
		egl_context: Option<&Arc<EglContext>>,
		w:           u32,
		h:           u32,
	)
	{
		let scale      = self.scale_factor.max( 1 ) as u32;
		let pw         = w.saturating_mul( scale );
		let ph         = h.saturating_mul( scale );
		let pool_bytes = (pw as u64)
			.saturating_mul( ph as u64 )
			.saturating_mul( 4 );

		const MAX_POOL: u64 = 256 * 1024 * 1024;
		if pool_bytes > MAX_POOL || pw == 0 || ph == 0
		{
			eprintln!( "[ltk] on_configure: size {w}×{h} (physical {pw}×{ph}) out of range — ignoring" );
			return;
		}
		let pool_bytes = pool_bytes as usize;

		if self.egl_surface.is_some()
		{
			// GPU path: resize the wl_egl_window so the next eglSwapBuffers
			// publishes a buffer of the new size. The canvas FBO is reallocated
			// by `Canvas::resize`, but only after we eglMakeCurrent — the draw
			// path will do that, so we just record the pending size here.
			if let Some( ref es ) = self.egl_surface
			{
				es.resize( pw as i32, ph as i32 );
			}
		} else if self.pool.is_some() {
			// SHM path: reallocate the pool to fit the new buffer size.
			match SlotPool::new( pool_bytes, shm )
			{
				Ok( pool ) => self.pool = Some( pool ),
				Err( e )   =>
				{
					eprintln!( "[ltk] SlotPool resize failed: {e} — keeping previous allocation" );
					return;
				}
			}
		} else {
			// First configure — pick the path. Try GPU when the bootstrap
			// succeeded; fall back to SHM if surface creation fails.
			let mut chose_gpu = false;
			if let Some( ctx ) = egl_context
			{
				match ctx.create_surface( self.surface.wl_surface(), pw as i32, ph as i32 )
				{
					Ok( es ) =>
					{
						self.egl_surface = Some( es );
						chose_gpu        = true;
					}
					Err( e ) =>
					{
						eprintln!( "[ltk] eglCreateWindowSurface failed: {e} — using SHM" );
					}
				}
			}
			if !chose_gpu
			{
				match SlotPool::new( pool_bytes, shm )
				{
					Ok( pool ) => self.pool = Some( pool ),
					Err( e )   =>
					{
						eprintln!( "[ltk] SlotPool init failed: {e}" );
						return;
					}
				}
			}
		}

		self.width  = w;
		self.height = h;
		self.surface.wl_surface().set_buffer_scale( self.scale_factor.max( 1 ) );
		self.configured    = true;
		self.needs_redraw  = true;
		self.content_dirty = true;
	}

	/// Request a redraw and mark the surface's content as dirty.
	///
	/// Use this from every site that potentially changes what the app's view
	/// returns (text edits, scroll offset, slider value, app messages, animation
	/// tick, configure). Pure hover/pressed transitions should leave
	/// `content_dirty` alone and only set `needs_redraw` directly so the draw
	/// pass can take the cheap partial-redraw path.
	pub( crate ) fn request_redraw( &mut self )
	{
		self.needs_redraw  = true;
		self.content_dirty = true;
	}
}
