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

//! Wayland touch → ltk dispatch.
//!
//! `wl_touch` down / up / motion map directly onto the three
//! lifecycle methods of [`GestureState`](super::gesture::GestureState),
//! same as pointer press / release / motion. Touch has no hover and
//! no scroll-axis event, so this file is shorter than `pointer.rs`;
//! the two handlers share `apply_move_outcome` /
//! `apply_release_events` in `dispatch.rs`.
//!
//! The first finger to land on a surface becomes its **primary slot**
//! and drives the single-slot gesture machine (swipe, scroll,
//! long-press, drag). Any additional finger arriving while the
//! primary is held bypasses the gesture machine and surfaces directly
//! through [`App::on_touch_down`] / [`App::on_touch_move`] /
//! [`App::on_touch_up`], so apps can implement pinch-zoom or
//! two-finger pan without losing the built-in gestures. Slot
//! ownership is recorded on `SurfaceState::primary_touch_id` plus
//! `SurfaceState::touch_slots` for the auxiliary fingers' last
//! position (Wayland `wl_touch.up` does not carry one, so the slot
//! cache supplies it).
//!
//! The cross-surface routing map `AppData::touch_focus` is still
//! per touch id, so an auxiliary slot that landed on an overlay
//! keeps reporting through that overlay's `App` callback even if
//! the finger drifts over the main surface.
//!
//! Apps that return `true` from [`App::claims_raw_touch`] opt out of
//! the primary-slot gesture machine entirely: every finger, primary
//! included, surfaces through the raw `on_touch_*` stream, and
//! widget presses, taps and swipes never fire. Positions are cached
//! in `SurfaceState::touch_slots` for all fingers in that mode so
//! `up` can report a release point.

use smithay_client_toolkit::seat::touch::TouchHandler;
use smithay_client_toolkit::reexports::client::
{
	protocol::{ wl_surface::WlSurface, wl_touch::WlTouch },
	Connection, QueueHandle,
};

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

impl<A: App> TouchHandler for AppData<A>
{
	fn down(
		&mut self,
		_conn:    &Connection,
		qh:       &QueueHandle<Self>,
		_touch:   &WlTouch,
		serial:   u32,
		_time:    u32,
		surface:  WlSurface,
		id:       i32,
		position: ( f64, f64 ),
	)
	{
		self.last_input_serial = serial;
		if self.is_secondary_surface( &surface ) { return; }
		let focus = self.focus_for_surface( &surface ).unwrap_or( SurfaceFocus::Main );
		self.touch_focus.insert( id, focus );
		let pos = self.surface( focus ).to_physical( position.0, position.1 );
		self.pointer_pos = pos;

		// Raw-touch apps take every finger verbatim; the gesture
		// machine never arms. Slots are tracked so `up` (which carries
		// no position in wl_touch) can report the release point.
		if self.app.claims_raw_touch()
		{
			self.surface_mut( focus ).touch_slots.insert( id, pos );
			self.app.on_touch_down( id as i64, pos.x, pos.y );
			return;
		}

		// Compositor re-grab after the origin surface died mid-drag: this
		// slot is already our primary and the drag state was migrated here,
		// so treat the redundant `down` as a continuation, not a restart.
		if self.surface( focus ).primary_touch_id == Some( id )
		{
			return;
		}

		// Auxiliary slot path: a second (or third…) finger arriving
		// while another finger already drives the primary slot stays
		// out of the single-slot gesture machine entirely. The app
		// gets a raw `on_touch_down` and can drive its own pinch /
		// pan from the per-id stream.
		let is_primary =
		{
			let ss = self.surface_mut( focus );
			if ss.primary_touch_id.is_none()
			{
				ss.primary_touch_id = Some( id );
				true
			}
			else
			{
				ss.touch_slots.insert( id, pos );
				false
			}
		};
		if !is_primary
		{
			self.app.on_touch_down( id as i64, pos.x, pos.y );
			return;
		}

		if matches!( focus, SurfaceFocus::Main ) && !self.overlays.is_empty()
		{
			self.dismiss_main_outside_popups( pos );
		}

		// Built-in context menu intercepts the touch before the
		// regular gesture machine — same logic as the pointer path.
		if self.surface( focus ).context_menu.is_some()
		{
			if self.handle_context_menu_press( focus, pos )
			{
				return;
			}
		}

		let outcome =
		{
			let ss = self.surface_mut( focus );
			let result = ss.gesture.on_press( pos, &ss.frame.widget_rects, &ss.frame.scroll_rects );
			ss.needs_redraw = true;
			result
		};
		self.set_focus( focus, outcome.hit_idx, qh );
		if let Some( msg ) = outcome.initial_slider_msg
		{
			self.pending_msgs.push( msg );
		}
		// Press-and-hold repeat — same wiring as the pointer path.
		if let Some( idx ) = outcome.hit_idx
		{
			let immediate = {
				let handlers = find_handlers( &self.surface( focus ).frame.widget_rects, idx );
				if matches!( handlers, Some( crate::widget::WidgetHandlers::Button { repeating: true, .. } ) )
				{
					handlers.and_then( |h| h.press_msg() )
				} else { None }
			};
			if let Some( msg ) = immediate
			{
				self.pending_msgs.push( msg );
				self.start_button_repeat( focus, idx );
			}
		}
		// Click-to-position the text cursor for touch presses too,
		// and double-tap selects the word under the press.
		if let Some( idx ) = outcome.hit_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
			{
				// Eye icon hit on a password field short-circuits
				// the text-edit dispatch — fire the toggle msg and
				// skip cursor placement.
				if self.handle_password_toggle_press( focus, idx, pos )
				{
					let _ = self.note_press_for_double_click( pos );
				}
				else
				{
					let is_double = self.note_press_for_double_click( pos );
					if is_double
					{
						self.handle_text_select_word( focus, idx, pos );
					} else {
						self.handle_text_pointer_down( focus, idx, pos );
					}
				}
			} else {
				let _ = self.note_press_for_double_click( pos );
			}
		} else {
			let _ = self.note_press_for_double_click( pos );
		}
	}

	fn up(
		&mut self,
		_conn:   &Connection,
		_qh:     &QueueHandle<Self>,
		_touch:  &WlTouch,
		_serial: u32,
		_time:   u32,
		id:      i32,
	)
	{
		let focus = self.touch_focus.remove( &id ).unwrap_or( SurfaceFocus::Main );
		if self.app.claims_raw_touch()
		{
			let pos =
			{
				let ss = self.surface_mut( focus );
				ss.touch_slots.remove( &id ).unwrap_or( self.pointer_pos )
			};
			self.app.on_touch_up( id as i64, pos.x, pos.y );
			return;
		}
		// Auxiliary release: not the primary slot → recover the last
		// recorded position for the up callback (Wayland's `wl_touch.up`
		// does not carry one) and bail before reaching the gesture
		// machine.
		let is_primary =
		{
			let ss = self.surface_mut( focus );
			ss.primary_touch_id == Some( id )
		};
		if !is_primary
		{
			let pos =
			{
				let ss = self.surface_mut( focus );
				ss.touch_slots.remove( &id ).unwrap_or( self.pointer_pos )
			};
			self.app.on_touch_up( id as i64, pos.x, pos.y );
			return;
		}

		// Touch-up does not carry a position in wl_touch — the last
		// motion's position is the release point.
		let pos         = self.pointer_pos;
		let global_drag = self.has_active_long_press_drag();
		let swipe       = self.swipe_config( focus );
		let events_out  =
		{
			let ss = self.surface_mut( focus );
			ss.needs_redraw      = true;
			ss.primary_touch_id  = None;
			ss.gesture.on_release( pos, &ss.frame.widget_rects, &swipe, global_drag )
		};
		self.apply_release_events( focus, events_out );
		self.stop_button_repeat();
	}

	fn motion(
		&mut self,
		_conn:    &Connection,
		_qh:      &QueueHandle<Self>,
		_touch:   &WlTouch,
		_time:    u32,
		id:       i32,
		position: ( f64, f64 ),
	)
	{
		let focus = *self.touch_focus.get( &id ).unwrap_or( &SurfaceFocus::Main );
		let pp    = self.surface( focus ).to_physical( position.0, position.1 );

		if self.app.claims_raw_touch()
		{
			self.pointer_pos = pp;
			{
				let ss = self.surface_mut( focus );
				ss.touch_slots.insert( id, pp );
			}
			self.app.on_touch_move( id as i64, pp.x, pp.y );
			return;
		}

		let is_primary =
		{
			let ss = self.surface( focus );
			ss.primary_touch_id == Some( id )
		};
		if !is_primary
		{
			// Auxiliary slot: cache the latest position so `up`'s
			// release point is accurate, then surface the raw motion.
			{
				let ss = self.surface_mut( focus );
				ss.touch_slots.insert( id, pp );
			}
			self.app.on_touch_move( id as i64, pp.x, pp.y );
			return;
		}

		self.pointer_pos = pp;
		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 (touch path).
		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 );
		}
	}

	fn shape(
		&mut self,
		_conn:   &Connection,
		_qh:     &QueueHandle<Self>,
		_touch:  &WlTouch,
		_id:     i32,
		_major:  f64,
		_minor:  f64,
	) {}

	fn orientation(
		&mut self,
		_conn:        &Connection,
		_qh:          &QueueHandle<Self>,
		_touch:       &WlTouch,
		_id:          i32,
		_orientation: f64,
	) {}

	fn cancel( &mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _touch: &WlTouch )
	{
		self.reset_touch_state();
	}
}

impl<A: App> AppData<A>
{
	/// Drop every in-flight touch gesture across all surfaces, notifying
	/// the app of any slot still down that it was tracking raw (auxiliary
	/// fingers, or every finger under `claims_raw_touch`). Shared by the
	/// `wl_touch.cancel` handler and the touch-capability add / remove path
	/// (suspend / resume on devices that power the touchscreen down): a
	/// yanked capability never delivers the pending `up` / `cancel`, so
	/// without this the stale `primary_touch_id` / gesture state strands and
	/// the first gesture after resume is misrouted and lost. Returns `true`
	/// when it actually had state to clear.
	pub( crate ) fn reset_touch_state( &mut self ) -> bool
	{
		let had_state = self.main.primary_touch_id.is_some()
			|| !self.main.touch_slots.is_empty()
			|| !self.touch_focus.is_empty()
			|| self.overlays.values().any( |ss| ss.primary_touch_id.is_some() || !ss.touch_slots.is_empty() );

		// Snapshot every auxiliary slot's last position so the app can be
		// notified with a meaningful release point, then drop every
		// per-surface slot in one pass.
		let mut aux_releases: Vec<( i32, crate::types::Point )> = Vec::new();
		let collect = |ss: &mut SurfaceState<A::Message>, out: &mut Vec<( i32, crate::types::Point )>|
		{
			for ( id, pos ) in ss.touch_slots.drain() { out.push( ( id, pos ) ); }
			ss.primary_touch_id = None;
			ss.gesture.on_cancel();
		};
		collect( &mut self.main, &mut aux_releases );
		for ss in self.overlays.values_mut()
		{
			collect( ss, &mut aux_releases );
		}
		self.touch_focus.clear();
		for ( id, pos ) in aux_releases
		{
			self.app.on_touch_up( id as i64, pos.x, pos.y );
		}
		self.stop_button_repeat();
		had_state
	}
}
