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

use crate::render::Canvas;
use crate::types::WidgetId;
use super::Element;

#[ cfg( test ) ]
mod tests;

/// Wraps any [`Element`] and emits a message on tap. Use when you want
/// click-to-emit on something richer than a [`Button`](super::button::Button)
/// — for example a [`Container`](super::container::Container) styled as a
/// card holding a row of icon + labels.
///
/// The wrapper is invisible to drawing: it delegates `preferred_size` and
/// rendering to the child. It does record a hit rect covering its full
/// allocated rect so taps anywhere inside fire `on_press`. Inner widgets
/// that are themselves interactive (e.g. a button nested inside the
/// pressable) keep priority — the layout pass pushes the wrapper's hit
/// rect *before* recursing into the child, and hit testing iterates in
/// reverse, so deeper widgets win.
///
/// No visual press feedback is applied — for state-driven appearance
/// changes use a [`Button`](super::button::Button) or compose with a
/// container that reacts to focus/press signals.
///
/// ```rust,no_run
/// # use ltk::{ column, container, pressable, row, Element, Pressable };
/// # #[ derive( Clone ) ] enum Msg { OpenWifiPicker }
/// # fn _ex(
/// #     icon:     Element<Msg>,
/// #     title:    Element<Msg>,
/// #     subtitle: Element<Msg>,
/// # ) -> Pressable<Msg> {
/// pressable(
///     container( row()
///         .push( icon )
///         .push( column().push( title ).push( subtitle ) ) )
///     .surface( "surface-card" )
///     .radius( 32.0 )
///     .padding_h( 16.5 )
///     .padding_v( 24.0 ),
/// )
/// .on_press( Msg::OpenWifiPicker )
/// # }
/// ```
pub struct Pressable<Msg: Clone>
{
	pub( crate ) child:          Box<Element<Msg>>,
	pub( crate ) on_press:       Option<Msg>,
	pub( crate ) on_long_press:  Option<Msg>,
	/// Drag-arm message. Fired alongside `on_long_press` when the touch
	/// hold timer elapses, AND fired by mouse left-button motion past
	/// the drag-promotion threshold without waiting for the timer. The
	/// caller uses this to arm any per-app drag state (in crustace,
	/// `dragging_item`); a widget that opens a context menu but isn't
	/// draggable leaves this `None`.
	pub( crate ) on_drag_start:  Option<Msg>,
	/// Keyboard `Escape`-key message. The runtime scans every laid-out
	/// pressable's snapshot and fires the topmost (highest `flat_idx`)
	/// `on_escape` it finds before the default ESC fallthrough chain.
	/// Used by [`crate::widget::dialog::Dialog`] to make `Esc` cancel a
	/// modal dialog without each app having to wire a global keyboard
	/// hook — but available to any composite that needs the same
	/// semantics.
	pub( crate ) on_escape:      Option<Msg>,
	/// Make the pressable hit-testable even when no callback is set.
	/// A `swallow=true` pressable consumes pointer events at its hit
	/// rect and emits no message — used by
	/// [`crate::widget::dialog::Dialog`] for the modal scrim plus the
	/// card-area swallow that prevents `dismiss_on_scrim` from firing
	/// when the user clicks on the dialog body itself. Has no effect
	/// when any handler is also set; in that case the handler determines
	/// the message and `swallow` is implicit.
	pub( crate ) swallow:        bool,
	pub( crate ) id:             Option<WidgetId>,
	pub( crate ) cursor:         Option<crate::types::CursorShape>,
}

impl<Msg: Clone> Pressable<Msg>
{
	pub fn new( child: impl Into<Element<Msg>> ) -> Self
	{
		Self
		{
			child:          Box::new( child.into() ),
			on_press:       None,
			on_long_press:  None,
			on_drag_start:  None,
			on_escape:      None,
			swallow:        false,
			id:             None,
			cursor:         None,
		}
	}

	/// Override the pointer cursor shape shown on hover.
	pub fn cursor( mut self, shape: crate::types::CursorShape ) -> Self
	{
		self.cursor = Some( shape );
		self
	}

	pub fn on_press( mut self, msg: Msg ) -> Self
	{
		self.on_press = Some( msg );
		self
	}

	pub fn on_long_press( mut self, msg: Msg ) -> Self
	{
		self.on_long_press = Some( msg );
		self
	}

	/// Attach a drag-arm message. Fires when the press transitions into
	/// drag mode — touch on hold-timer expiry, mouse on motion past the
	/// drag-promotion threshold. Independent of `on_long_press` so a
	/// widget can open a menu without becoming draggable, or be
	/// draggable without opening a menu.
	pub fn on_drag_start( mut self, msg: Msg ) -> Self
	{
		self.on_drag_start = Some( msg );
		self
	}

	/// Bind a keyboard-`Escape` message to this pressable. See
	/// [`Pressable::on_escape`] for the dispatch order semantics.
	pub fn on_escape( mut self, msg: Msg ) -> Self
	{
		self.on_escape = Some( msg );
		self
	}

	/// Make the pressable hit-testable even when no `on_press` /
	/// `on_long_press` / `on_drag_start` is configured. See
	/// [`Pressable::swallow`].
	pub fn swallow( mut self, on: bool ) -> Self
	{
		self.swallow = on;
		self
	}

	pub fn id( mut self, id: WidgetId ) -> Self
	{
		self.id = Some( id );
		self
	}

	pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> ( f32, f32 )
	{
		self.child.preferred_size( max_width, canvas )
	}

	/// True when the wrapper participates in hit-testing — has at least
	/// one pointer / keyboard handler set, or has been opted in to
	/// silent swallow via [`Pressable::swallow`]. Used by the layout
	/// pass to skip pushing a hit rect for a no-op pressable.
	pub fn has_handler( &self ) -> bool
	{
		self.on_press.is_some()
			|| self.on_long_press.is_some()
			|| self.on_drag_start.is_some()
			|| self.on_escape.is_some()
			|| self.swallow
	}

	pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> Pressable<U>
	where
		U: Clone + 'static,
		Msg: 'static,
	{
		Pressable
		{
			child:         Box::new( self.child.map_arc( f ) ),
			on_press:      self.on_press.map( |m| ( *f )( m ) ),
			on_long_press: self.on_long_press.map( |m| ( *f )( m ) ),
			on_drag_start: self.on_drag_start.map( |m| ( *f )( m ) ),
			on_escape:     self.on_escape.map( |m| ( *f )( m ) ),
			swallow:       self.swallow,
			id:            self.id,
			cursor:        self.cursor,
		}
	}
}

pub fn pressable<Msg: Clone>( child: impl Into<Element<Msg>> ) -> Pressable<Msg>
{
	Pressable::new( child )
}

impl<Msg: Clone + 'static> From<Pressable<Msg>> for Element<Msg>
{
	fn from( p: Pressable<Msg> ) -> Self
	{
		Element::Pressable( p )
	}
}
