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

use smithay_client_toolkit::reexports::client::QueueHandle;

use super::app_data::AppData;
use super::surface::SurfaceFocus;
use crate::app::{ App, OverlayId };
use crate::tree::find_handlers;
use crate::types::Point;
use crate::widget::WidgetHandlers;

impl<A: App> AppData<A>
{
	/// Push `on_dismiss` for every active xdg-popup overlay whose
	/// anchor widget was not hit by a press at `pos` (logical pixels
	/// in main-surface space). Used to compensate for compositors
	/// (notably Mutter) that route pointer button events to the main
	/// surface while a popup grab is technically active, instead of
	/// breaking the grab. The check skips presses on the trigger pill
	/// itself so the trigger's own `toggle` message keeps owning that
	/// transition.
	pub( crate ) fn dismiss_main_outside_popups( &mut self, pos: Point )
	{
		if self.overlays.is_empty() { return; }
		let specs = self.app.overlays();
		for spec in specs
		{
			// Only xdg-popups need the main-side fallback: they grab
			// the seat and rely on the compositor to break the grab on
			// outside input, which Mutter does not do reliably. Layer-
			// shell overlays own their own `input_region` and dispatch
			// dismissal from the overlay tap path, so firing it again
			// from a press on the main surface produces a duplicate
			// dismiss that races other intents (e.g. forge's topbar
			// taps that route a separate IPC into the same overlay).
			let Some( anchor_id ) = spec.anchor_widget_id else { continue; };
			let anchor_rect = self.main.frame.widget_rects.iter()
				.find( | w | w.id == Some( anchor_id ) )
				.map( | w | w.rect );
			let on_anchor = anchor_rect
				.map( | r | pos.x >= r.x && pos.x < r.x + r.width
				          && pos.y >= r.y && pos.y < r.y + r.height )
				.unwrap_or( false );
			if !on_anchor
			{
				if let Some( msg ) = spec.on_dismiss
				{
					self.pending_msgs.push( msg );
				}
			}
		}
	}

	/// Push `on_dismiss` for every active xdg-popup overlay
	/// unconditionally (used by keyboard Escape handling — there is
	/// no spatial test to do).
	pub( crate ) fn dismiss_all_popups( &mut self )
	{
		if self.overlays.is_empty() { return; }
		let specs = self.app.overlays();
		for spec in specs
		{
			if spec.anchor_widget_id.is_some()
			{
				if let Some( msg ) = spec.on_dismiss
				{
					self.pending_msgs.push( msg );
				}
			}
		}
	}

	/// Look up the dismiss message for an overlay (tap on empty area).
	/// Returns `None` if the overlay has no `on_dismiss` set or was removed.
	pub( crate ) fn overlay_dismiss_msg( &self, id: OverlayId ) -> Option<A::Message>
	{
		self.app.overlays().into_iter()
			.find( |s| s.id == id )
			.and_then( |s| s.on_dismiss )
	}

	pub( crate ) fn set_focus( &mut self, focus: SurfaceFocus, idx: Option<usize>, qh: &QueueHandle<Self> )
	{
		let was_text_input;
		let is_text_input;
		let secure;
		{
			let ss = self.surface_mut( focus );

			is_text_input = idx
				.and_then( |i| find_handlers( &ss.frame.widget_rects, i ) )
				.map( |h| h.is_text_input() )
				.unwrap_or( false );
			secure = idx
				.and_then( |i| find_handlers( &ss.frame.widget_rects, i ) )
				.map( |h| matches!( h, WidgetHandlers::TextEdit { secure: true, .. } ) )
				.unwrap_or( false );
			was_text_input = ss.focused_idx
				.and_then( |i| find_handlers( &ss.frame.widget_rects, i ) )
				.map( |h| h.is_text_input() )
				.unwrap_or( false );

			// Clear pending text value when losing focus
			if let Some( prev_idx ) = ss.focused_idx
			{
				if idx != Some( prev_idx )
				{
					ss.pending_text_values.remove( &prev_idx );
				}
			}

			ss.focused_idx = idx;
			ss.focused_id  = idx.and_then( |i|
			{
				ss.frame.widget_rects.iter()
					.find( |w| w.flat_idx == i )
					.and_then( |w| w.id )
			} );
			ss.request_redraw();

			// Sync cursor to the end of the value when focusing a text
			// input, via the `usize::MAX` sentinel ("end of whatever
			// value is rendered" — every consumer clamps with
			// `cursor.min( value.len() )`). A concrete `value.len()`
			// snapshot here would go stale if the value keeps growing
			// after focus (e.g. updated externally over IPC), leaving
			// later insertions mid-string. Default behaviour collapses
			// the selection to the cursor — focus changes always
			// discard any prior selection state. Fields built with
			// `.select_on_focus( true )` instead anchor the selection
			// at `0` so the whole value is highlighted, ready to be
			// replaced by the next keystroke (typical for numeric
			// pickers).
			if is_text_input
			{
				if let Some( i ) = idx
				{
					let handler = find_handlers( &ss.frame.widget_rects, i );
					let anchor  = match handler
					{
						Some( WidgetHandlers::TextEdit { select_on_focus: true, .. } ) => 0,
						_ => usize::MAX,
					};
					ss.frame.cursor_state.insert( i, usize::MAX );
					ss.frame.selection_anchor.insert( i, anchor );
				}
			}
		}

		if was_text_input && !is_text_input
		{
			self.deactivate_text_input();
			self.app.on_text_input_focused( false );
			self.dirty_caches();
		}
		else if is_text_input && !was_text_input
		{
			self.activate_text_input( qh, secure );
			self.app.on_text_input_focused( true );
			self.dirty_caches();
		}
		else if is_text_input
		{
			// Focus moved between text fields: refresh the content type so a
			// password field is flagged even without re-activating.
			self.activate_text_input( qh, secure );
		}
	}
}
