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

use smithay_client_toolkit::seat::pointer::PointerEvent;
use smithay_client_toolkit::reexports::client::
{
	protocol::wl_pointer::WlPointer,
	Connection, QueueHandle,
};

use crate::app::App;
use crate::event_loop::{ AppData, SurfaceFocus };
use crate::tree::{ find_handlers, find_widget_at };

use super::helpers::hover_affects_paint;

impl<A: App> AppData<A>
{
	pub( super ) fn on_pointer_motion(
		&mut self,
		_conn:    &Connection,
		_qh:      &QueueHandle<Self>,
		_pointer: &WlPointer,
		event:    &PointerEvent,
	)
	{
		let focus = self.focus_for_surface( &event.surface )
			.unwrap_or( SurfaceFocus::Main );
		let pp = self.surface( focus ).to_physical( event.position.0, event.position.1 );
		self.pointer_pos = pp;
		self.app.on_pointer_move( pp.x, pp.y );

		// Hover tracking — pointer-only (touch has no hover).
		// Runs before the gesture motion so the cache-dirty
		// below picks up any hover-dependent redraw request.
		let new_hover = find_widget_at( &self.surface( focus ).frame.widget_rects, pp );
		let old_hover = self.surface( focus ).hovered_idx;
		if new_hover != old_hover
		{
			let redraw = hover_affects_paint( &self.surface( focus ).frame.widget_rects, old_hover )
				|| hover_affects_paint( &self.surface( focus ).frame.widget_rects, new_hover );
			{
				let ss = self.surface_mut( focus );
				ss.hovered_idx  = new_hover;
				if redraw { ss.needs_redraw = true; }
			}
			match new_hover
			{
				Some( idx ) => self.arm_tooltip( focus, idx ),
				None        => self.cancel_tooltip(),
			}
		}

		// Mouse drag-promotion: a left-button press whose hit
		// widget carries `on_drag_start` should arm a drag as
		// soon as the cursor moves past the threshold, without
		// waiting for the touch hold timer. Touch keeps its
		// hold-then-drag path because scroll / swipe gestures
		// need the in-between motion budget; mouse has a
		// dedicated right-click for the menu so left-button
		// can be drag-only.
		//
		// Threshold of 24 px (logical) sits comfortably above
		// the gesture machine's 6 px long-press cancel
		// tolerance and above crustace's 16 px drag-commit
		// threshold, so by the time we promote the next
		// motion sample is already past the app's commit
		// distance and drag mode latches without flashing
		// any half-state.
		//
		// Promotion is synchronous (`self.app.update(...)`
		// directly) so the app's drag state is armed BEFORE
		// the `on_drag_move` call below runs — otherwise the
		// seed coords land on a `dragging_item = None`
		// shell and get lost. We pay the cost of bypassing
		// `invalidate_after` for this one msg, but the next
		// frame will repaint everything anyway because the
		// drag is in flight.
		let promote = {
			let ss = self.surface( focus );
			match ( ss.gesture.long_press_origin, ss.gesture.drag_start_msg.is_some() )
			{
				( Some( o ), true ) => ( pp.x - o.x ).hypot( pp.y - o.y ) > 24.0,
				_                   => false,
			}
		};
		if promote
		{
			let ( ds_msg, origin ) = {
				let ss = self.surface_mut( focus );
				let m = ss.gesture.drag_start_msg.take().expect( "promote checked is_some" );
				let o = ss.gesture.long_press_origin.expect( "promote checked Some(origin)" );
				ss.gesture.long_press_start    = None;
				ss.gesture.long_press_origin   = None;
				ss.gesture.long_press_msg      = None;
				ss.gesture.long_press_text_idx = None;
				ss.gesture.long_press_fired    = true;
				ss.request_redraw();
				( m, o )
			};
			self.app.update( ds_msg );
			let ( ox, oy ) = self.surface_offset_for( focus );
			self.app.on_drag_move( origin.x + ox, origin.y + oy );
			self.dirty_caches();
			self.stop_button_repeat();
		}

		// `global_drag` must be sampled AFTER the promotion
		// above — promotion flips `long_press_fired` and we
		// want the gesture machine to take the drag branch
		// for the same motion event that triggered the
		// promotion.
		let global_drag = self.has_active_long_press_drag();
		let swipe       = self.swipe_config( focus );
		let outcome =
		{
			let ss = self.surface_mut( focus );
			ss.gesture.on_move( pp, &ss.frame.widget_rects, &mut ss.frame.scroll_offsets, &swipe, global_drag )
		};
		self.apply_move_outcome( focus, outcome );

		// Drag-to-select inside a TextEdit. Runs after the
		// gesture machine so the gesture's "did we leave
		// the press's hit rect?" reasoning still applies
		// to the press itself; for text fields the answer
		// is "fine, keep selecting" because we only widen
		// the selection while the pointer is still inside
		// the same widget rect.
		let pressed_text = self.surface( focus ).gesture.pressed_idx
			.and_then( |idx|
			{
				let is_text = find_handlers( &self.surface( focus ).frame.widget_rects, idx )
					.map( |h| h.is_text_input() ).unwrap_or( false );
				if is_text { Some( idx ) } else { None }
			} );
		if let Some( idx ) = pressed_text
		{
			self.handle_text_pointer_drag( focus, idx, pp );
		}
		// Cursor shape: hover may have changed → push the
		// new shape to the compositor (no-op when
		// unchanged).
		self.dispatch_cursor_shape( focus );
	}
}
