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

use std::sync::Arc;
use crate::types::{ Length, Point, Rect };
use super::{ slider, text };

/// Per-leaf interaction snapshot captured during layout. One variant per
/// interactive widget kind; the layout pass clones the relevant callbacks /
/// values from the [`Element`](super::Element) tree into here so input handlers can dispatch
/// in O(1) without re-walking the tree.
///
/// `None` is used for focusable widgets that emit no message (e.g. a disabled
/// button or a focusable container) — the entry still appears in
/// `widget_rects` for hit testing and focus traversal.
pub enum WidgetHandlers<Msg: Clone>
{
	None,
	Button
	{
		on_press:      Option<Msg>,
		on_long_press: Option<Msg>,
		on_drag_start: Option<Msg>,
		/// Keyboard `Escape`-key message — the runtime scans every laid-out
		/// `Button` snapshot in reverse and fires the first non-`None`
		/// `on_escape` it finds before the default ESC fallthrough chain.
		/// Currently sourced from
		/// [`crate::widget::pressable::Pressable::on_escape`]; native
		/// [`crate::widget::button::Button`] always sets this to `None`.
		on_escape:     Option<Msg>,
		repeating:     bool,
	},
	Toggle   { on_toggle: Option<Msg>, value: bool },
	Checkbox { on_toggle: Option<Msg>, value: bool },
	Radio    { on_select: Option<Msg>, selected: bool },
	ListItem { on_press:  Option<Msg> },
	WindowButton { on_press: Option<Msg> },
	TextEdit
	{
		value:     String,
		on_change: Option<Arc<dyn Fn( String ) -> Msg>>,
		on_submit: Option<Msg>,
		/// `true` when the source `TextEdit` was built with
		/// `.secure( true )`. Propagates the wipe-on-drop behaviour to
		/// every per-frame handler snapshot so credential text does not
		/// linger across multiple cloned `WidgetHandlers` allocations.
		secure:    bool,
		/// `true` when the source `TextEdit` was built with
		/// `.multiline( true )`. The keyboard dispatch reads this so
		/// pressing Enter inserts a `\n` instead of firing
		/// [`Self::submit_msg`]. Mutually exclusive with `secure`.
		multiline: bool,
		/// Horizontal alignment snapshot — needed by the runtime's
		/// hit-testing path so a click on a centred / right-aligned
		/// field lands on the correct glyph.
		align:     text::TextAlign,
		/// Font size snapshot — needed by the hit-testing path so
		/// the runtime measures glyphs at the same size the renderer
		/// drew them. `None` for fields that do not call
		/// `.font_size( … )` (they follow the widget-scaling mode).
		font_size: Option<Length>,
		/// `true` when the source field opted into select-all-on-
		/// focus. The runtime reads this in `set_focus` to decide
		/// whether the new selection should anchor at `0` (replace
		/// on next keystroke) or at the cursor (insert).
		select_on_focus: bool,
		/// Snapshot of the
		/// [`crate::widget::text_edit::TextEdit::password_toggle`]
		/// callback. When `Some`, pointer / touch dispatch checks
		/// the eye-icon hit zone (via
		/// [`crate::widget::text_edit::password_toggle_hit_zone`])
		/// before falling through to cursor placement and fires
		/// this message instead.
		password_toggle_msg: Option<Msg>,
	},
	Slider
	{
		on_change: Option<Arc<dyn Fn( f32 ) -> Msg>>,
		/// Fired once when the drag ends, with the final value. Lets an app
		/// keep an expensive commit (a subprocess, a D-Bus round trip, a
		/// compositor reconfigure) off the per-motion path.
		on_release: Option<Arc<dyn Fn( f32 ) -> Msg>>,
		axis:      slider::SliderAxis,
		value:     f32,
		/// Thumb size resolved through the widget-scaling mode at layout
		/// time. Feeds `value_from_pos_in_rect` so a click maps against the
		/// same thumb the renderer drew. Unused by the vertical axis.
		thumb_px:  f32,
	},
}

impl<Msg: Clone> Drop for WidgetHandlers<Msg>
{
	/// Mirror the wipe-on-drop behaviour of [`super::text_edit::TextEdit`] for the
	/// per-frame handler snapshots the runtime keeps. When the snapshot
	/// was built from a secure text edit we scrub the value bytes here so
	/// the heap allocation that backs the cloned `String` is overwritten
	/// before it is returned to the allocator. Non-secure variants run an
	/// inert path; the field-level drops that fire after this fn handle
	/// the rest of the data.
	fn drop( &mut self )
	{
		if let WidgetHandlers::TextEdit { value, secure: true, .. } = self
		{
			// SAFETY: identical reasoning to `TextEdit::drop` — we only
			// write zeros into the underlying byte buffer, leaving valid
			// UTF-8 (NUL bytes) in place until the auto-drop frees it.
			let bytes = unsafe { value.as_mut_vec() };
			crate::secure_mem::secure_zero( bytes );
		}
	}
}

impl<Msg: Clone> Clone for WidgetHandlers<Msg>
{
	fn clone( &self ) -> Self
	{
		match self
		{
			WidgetHandlers::None              => WidgetHandlers::None,
			WidgetHandlers::Button   { on_press, on_long_press, on_drag_start, on_escape, repeating } => WidgetHandlers::Button
			{
				on_press:      on_press.clone(),
				on_long_press: on_long_press.clone(),
				on_drag_start: on_drag_start.clone(),
				on_escape:     on_escape.clone(),
				repeating:     *repeating,
			},
			WidgetHandlers::Toggle   { on_toggle, value } => WidgetHandlers::Toggle   { on_toggle: on_toggle.clone(), value: *value },
			WidgetHandlers::Checkbox { on_toggle, value } => WidgetHandlers::Checkbox { on_toggle: on_toggle.clone(), value: *value },
			WidgetHandlers::Radio    { on_select, selected } => WidgetHandlers::Radio { on_select: on_select.clone(), selected: *selected },
			WidgetHandlers::ListItem { on_press  } => WidgetHandlers::ListItem { on_press:  on_press.clone()  },
			WidgetHandlers::WindowButton { on_press } => WidgetHandlers::WindowButton { on_press: on_press.clone() },
			WidgetHandlers::TextEdit { value, on_change, on_submit, secure, multiline, align, font_size, select_on_focus, password_toggle_msg } =>
			{
				WidgetHandlers::TextEdit
				{
					value:               value.clone(),
					on_change:           on_change.clone(),
					on_submit:           on_submit.clone(),
					secure:              *secure,
					multiline:           *multiline,
					align:               *align,
					font_size:           *font_size,
					select_on_focus:     *select_on_focus,
					password_toggle_msg: password_toggle_msg.clone(),
				}
			}
			WidgetHandlers::Slider { on_change, on_release, axis, value, thumb_px } =>
			{
				WidgetHandlers::Slider
				{
					on_change:  on_change.clone(),
					on_release: on_release.clone(),
					axis:       *axis,
					value:      *value,
					thumb_px:   *thumb_px,
				}
			}
		}
	}
}

impl<Msg: Clone> WidgetHandlers<Msg>
{
	pub fn is_text_input( &self ) -> bool
	{
		matches!( self, WidgetHandlers::TextEdit { .. } )
	}

	pub fn is_disabled( &self ) -> bool
	{
		match self
		{
			WidgetHandlers::Button   { on_press, on_long_press, on_drag_start, on_escape, .. } =>
				on_press.is_none() && on_long_press.is_none() && on_drag_start.is_none() && on_escape.is_none(),
			WidgetHandlers::Toggle   { on_toggle, .. } => on_toggle.is_none(),
			WidgetHandlers::Checkbox { on_toggle, .. } => on_toggle.is_none(),
			WidgetHandlers::Radio    { on_select, .. } => on_select.is_none(),
			WidgetHandlers::ListItem { on_press } => on_press.is_none(),
			WidgetHandlers::WindowButton { on_press } => on_press.is_none(),
			WidgetHandlers::Slider   { on_change, .. } => on_change.is_none(),
			WidgetHandlers::TextEdit { on_change, on_submit, .. } => on_change.is_none() && on_submit.is_none(),
			WidgetHandlers::None => true,
		}
	}

	/// `true` when this is a [`WidgetHandlers::TextEdit`] whose source
	/// widget was built with `.multiline( true )`. The keyboard
	/// dispatch reads this so pressing Enter inserts a `\n` instead of
	/// submitting.
	pub fn is_multiline_text_input( &self ) -> bool
	{
		matches!( self, WidgetHandlers::TextEdit { multiline: true, .. } )
	}

	pub fn is_slider( &self ) -> bool
	{
		matches!( self, WidgetHandlers::Slider { .. } )
	}

	/// `true` when this widget is a row inside a scrollable list that
	/// keyboard arrow navigation should treat as a stepping point.
	/// Currently restricted to [`ListItem`](crate::ListItem); buttons,
	/// toggles, sliders, etc. are not stepped over by Arrow Up/Down so
	/// they do not interfere with the row-by-row navigation pattern in
	/// combo popups, settings menus and similar lists.
	pub fn is_navigable_list_item( &self ) -> bool
	{
		matches!( self, WidgetHandlers::ListItem { .. } )
	}

	/// Convenience: extract the press / activation message for the variants
	/// that have one (Button, Toggle, Checkbox, Radio, ListItem). Returns
	/// `None` for sliders / text edits / `None` / disabled widgets.
	pub fn press_msg( &self ) -> Option<Msg>
	{
		match self
		{
			WidgetHandlers::Button   { on_press, .. } => on_press.clone(),
			WidgetHandlers::Toggle   { on_toggle, .. } => on_toggle.clone(),
			WidgetHandlers::Checkbox { on_toggle, .. } => on_toggle.clone(),
			WidgetHandlers::Radio    { on_select, .. } => on_select.clone(),
			WidgetHandlers::ListItem { on_press  } => on_press.clone(),
			WidgetHandlers::WindowButton { on_press } => on_press.clone(),
			_                                       => None,
		}
	}

	/// Submit message (Enter on a focused TextEdit). `None` for every other
	/// variant.
	pub fn submit_msg( &self ) -> Option<Msg>
	{
		match self
		{
			WidgetHandlers::TextEdit { on_submit, .. } => on_submit.clone(),
			_                                          => None,
		}
	}

	/// Build the `on_change` message for a TextEdit given the new value.
	pub fn text_change_msg( &self, new_value: &str ) -> Option<Msg>
	{
		match self
		{
			WidgetHandlers::TextEdit { on_change: Some( f ), .. } => Some( f( new_value.to_string() ) ),
			_ => None,
		}
	}

	/// Build the `on_change` message for a Slider given a value in `[0,1]`.
	pub fn slider_change_msg( &self, value: f32 ) -> Option<Msg>
	{
		match self
		{
			WidgetHandlers::Slider { on_change: Some( f ), .. } => Some( f( value ) ),
			_ => None,
		}
	}

	/// Build the `on_release` message for a Slider given its final value.
	pub fn slider_release_msg( &self, value: f32 ) -> Option<Msg>
	{
		match self
		{
			WidgetHandlers::Slider { on_release: Some( f ), .. } => Some( f( value ) ),
			_ => None,
		}
	}

	/// Compute the `[0.0, 1.0]` value for the slider this handler belongs to,
	/// given a pointer position inside its layout rect. Dispatches on the
	/// stored [`slider::SliderAxis`] so the same call site in `input.rs` drives
	/// both horizontal [`Slider`](slider::Slider) and vertical
	/// [`VSlider`](super::vslider::VSlider).
	///
	/// Returns `0.0` for non-slider variants — callers combine this with
	/// [`Self::slider_change_msg`], which also gates on the variant, so the
	/// zero is never consumed in practice.
	pub fn slider_value_from_pos( &self, rect: Rect, pos: Point ) -> f32
	{
		match self
		{
			WidgetHandlers::Slider { axis, thumb_px, .. } =>
			{
				slider::value_from_pos_in_rect( rect, pos, *axis, *thumb_px )
			}
			_ => 0.0,
		}
	}

	/// Store the widget-scaling-resolved thumb size into a horizontal
	/// [`WidgetHandlers::Slider`] snapshot. Called by the layout pass (which
	/// carries a canvas) so the input path maps clicks against the same
	/// thumb the renderer drew. A no-op for every other variant.
	pub( crate ) fn set_slider_thumb_px( &mut self, px: f32 )
	{
		if let WidgetHandlers::Slider { thumb_px, .. } = self
		{
			*thumb_px = px;
		}
	}

	/// `true` when this is a [`WidgetHandlers::Button`] whose source
	/// widget opted into press-and-hold repeat. The runtime reads
	/// this on press to decide whether to fire `press_msg`
	/// immediately + arm a calloop repeat timer, and on release to
	/// suppress the regular tap-on-release fire.
	pub fn is_repeating( &self ) -> bool
	{
		matches!( self, WidgetHandlers::Button { repeating: true, .. } )
	}

	/// Long-press / right-click message for this widget, or `None` if
	/// none configured. Currently only [`WidgetHandlers::Button`]
	/// carries one. Firing this does not by itself put the press into
	/// drag mode — see [`Self::drag_start_msg`] for that.
	pub fn long_press_msg( &self ) -> Option<Msg>
	{
		match self
		{
			WidgetHandlers::Button { on_long_press, .. } => on_long_press.clone(),
			_                                            => None,
		}
	}

	/// Drag-arm message for this widget, or `None` if none configured.
	/// Fired by the touch hold-timer alongside `long_press_msg`, and
	/// by mouse left-button motion past the drag-promotion threshold
	/// (without firing the menu). Promotes the gesture into drag mode.
	pub fn drag_start_msg( &self ) -> Option<Msg>
	{
		match self
		{
			WidgetHandlers::Button { on_drag_start, .. } => on_drag_start.clone(),
			_                                            => None,
		}
	}

	/// Keyboard `Escape` message for this widget, or `None` if none
	/// configured. Used by the keyboard ESC handler to scan
	/// `widget_rects` for a [`crate::widget::dialog::Dialog`] (or other
	/// `Pressable::on_escape`-bearing wrapper) and fire its cancel
	/// message before the default ESC fallthrough chain.
	pub fn escape_msg( &self ) -> Option<Msg>
	{
		match self
		{
			WidgetHandlers::Button { on_escape, .. } => on_escape.clone(),
			_                                        => None,
		}
	}

	/// Current text-edit value (for cursor placement on focus, backspace
	/// rebuild, etc.). `None` for non-text-edit variants.
	pub fn current_value( &self ) -> Option<&str>
	{
		match self
		{
			WidgetHandlers::TextEdit { value, .. } => Some( value.as_str() ),
			_ => None,
		}
	}
}
