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

//! Text input field — single-line or multiline. The widget itself
//! owns layout / draw; the runtime side of text editing
//! (insert, delete, cursor movement, selection, clipboard)
//! lives in the `event_loop::text_editing` private module.

use std::sync::Arc;

use crate::render::Canvas;
use crate::secure_mem::secure_zero;
use crate::types::{ Length, Rect, WidgetId };

use super::Element;

pub( crate ) mod theme;
pub( crate ) mod wrapping;
pub( crate ) mod hit_test;
pub( crate ) mod cursor;
mod draw;
#[ cfg( test ) ]
mod tests;

pub use draw::password_toggle_hit_zone;
pub( crate ) use hit_test::byte_offset_at;
pub( crate ) use cursor::{ cursor_visual_down, cursor_visual_end, cursor_visual_home, cursor_visual_up };

/// Resolve the font size into a concrete logical px. `Some( len )` resolves
/// against [`Canvas::viewport_logical`] — the font space, so `× dpi_scale` at
/// raster yields the right physical size — exactly like the button label;
/// `None` follows the widget-scaling mode at the theme default. Shared by the
/// draw and hit-test paths, which all carry a canvas.
pub( crate ) fn resolve_font_size( canvas: &Canvas, fs: Option<Length> ) -> f32
{
	fs
		.map( |l| canvas.resolve_font( l ) )
		.unwrap_or_else( || canvas.font_px( theme::FONT_SIZE ) )
}

/// A text input field.
///
/// Single-line by default; switches to a multi-row text-area via
/// [`Self::multiline`]. Single-line mode honours the optional inline
/// builders for picker-style fields:
///
/// * [`Self::align`] — horizontal alignment of the displayed text;
/// * [`Self::borderless`] — drop the surrounding pill / border so the
///   field can sit inside a parent that already paints its own
///   surface;
/// * [`Self::fixed_width`] — pin the preferred width to a specific
///   number of pixels instead of claiming `max_width`;
/// * [`Self::font_size`] — override the text font size;
/// * [`Self::select_on_focus`] — auto-select the value on focus so
///   the next keystroke replaces it (numeric pickers, short-form
///   inputs).
/// * [`Self::password_toggle`] — pin a built-in show / hide-password
///   eye icon to the right edge of the field; the bullet
///   substitution flips with the externally-owned `visible` state
///   on each tap.
///
/// ```rust,no_run
/// # use ltk::{ text_edit, Element };
/// # #[ derive( Clone ) ] enum Msg { UsernameChanged( String ), Submit }
/// # struct App { username: String }
/// # impl App { fn _ex( &self ) -> Element<Msg> {
/// text_edit( "Username", &self.username )
///     .on_change( |s| Msg::UsernameChanged( s ) )
///     .on_submit( Msg::Submit )
/// .into()
/// # }}
/// ```
///
/// ## Password field with show / hide toggle
///
/// ```rust,no_run
/// # use ltk::{ text_edit, Element };
/// # #[ derive( Clone ) ] enum Msg { PasswordChanged( String ), TogglePassword }
/// # struct App { password: String, password_visible: bool }
/// # impl App { fn _ex( &self ) -> Element<Msg> {
/// text_edit( "Password", &self.password )
///     .on_change( |s| Msg::PasswordChanged( s ) )
///     .password_toggle( self.password_visible, Msg::TogglePassword )
/// .into()
/// # }}
/// ```
///
/// `password_toggle` overrides [`Self::secure`] when both are set —
/// the toggle's `visible` parameter drives the bullet substitution
/// from then on. The widget still wipes the buffer on drop and
/// skips the IME registration (the same hardening
/// [`Self::secure`] gives) regardless of the current visibility,
/// so flipping the eye does not weaken the field's threat model
/// at runtime.
pub struct TextEdit<Msg: Clone>
{
	/// Placeholder text shown when the field is empty.
	pub( crate ) placeholder: String,
	/// Current field value.
	pub( crate ) value:       String,
	/// Callback invoked with the new value on every keystroke.
	/// `Arc` (not `Box`) so the layout pass can clone it into the per-leaf
	/// handler snapshot for O(1) dispatch on input events.
	pub( crate ) on_change:   Option<Arc<dyn Fn(String) -> Msg>>,
	/// Message emitted when the user presses Enter.
	pub( crate ) on_submit:   Option<Msg>,
	/// When `true`, the value is rendered as bullet characters (password mode).
	pub( crate ) secure:      bool,
	/// When `true`, the widget renders as a multi-row text area: the
	/// box grows to [`Self::rows`] visible rows, line breaks in the
	/// value are honoured at draw time, and pressing Enter inserts a
	/// `\n` rather than firing [`Self::on_submit`]. Ignored when
	/// [`Self::secure`] is set — passwords are always single-line.
	pub( crate ) multiline:   bool,
	/// Visible row count when `multiline` is `true`. Drives
	/// `preferred_size`'s height calculation so a multiline field
	/// claims a sensible vertical slot in the parent layout. Ignored
	/// when `multiline` is `false`.
	pub( crate ) rows:        u32,
	/// Byte offset of the text cursor within `value` (used by insert_str/backspace).
	pub( crate ) cursor_pos:  usize,
	/// Optional stable identifier for focus management.
	pub( crate ) id:          Option<WidgetId>,
	/// Override the pointer cursor shape on hover. `None` falls back
	/// to the I-beam default that matches every desktop convention.
	pub( crate ) cursor:      Option<crate::types::CursorShape>,
	/// Horizontal alignment of the displayed text inside the inner
	/// content rect. Only takes effect on the single-line path when
	/// the value fits inside the inner width — once the value
	/// overflows, the internal `single_line_scroll_x` helper takes
	/// over and the alignment offset collapses to `0` so scrolling
	/// reads naturally. Default `TextAlign::Left`.
	pub( crate ) align:       super::text::TextAlign,
	/// Skip the field's background fill and border stroke. Useful
	/// when the [`TextEdit`] is dropped inside another container
	/// that paints its own surface (e.g. the digit cells inside
	/// [`crate::widget::time_picker::TimePicker`]) and a second pill
	/// would only add visual noise.
	pub( crate ) borderless:  bool,
	/// Override the preferred width reported to the parent layout.
	/// Without this the single-line `TextEdit` claims `max_width` and
	/// fills whatever rect the parent allocates — which is the right
	/// default for forms but wrong when the field needs to be sized
	/// to fit a fixed number of glyphs (date / time pickers, inline
	/// numeric inputs).
	pub( crate ) fixed_width: Option<Length>,
	/// Font size in pixels for the single-line draw path. `0.0` (the
	/// Label font size. `None` follows the process [`crate::WidgetScaling`]
	/// mode at the theme default (via [`crate::Canvas::font_px`]); a
	/// [`Length`] pins it and is resolved in font space
	/// ([`crate::Canvas::viewport_logical`]), exactly like the button label.
	/// Resolved via [`Self::effective_font_size`] by every consumer, all of
	/// which carry a canvas. Multiline mode ignores this and always uses the
	/// theme constant.
	pub( crate ) font_size:   Option<Length>,
	/// Optional single-line field height. `None` uses the theme default
	/// (`theme::HEIGHT`); a [`Length`] scales the box with the surface, so
	/// a field can match a fluid button or grow with a fluid form. Resolved
	/// in physical layout space, like all geometry. Ignored in multiline
	/// mode, where the height follows [`Self::rows`].
	pub( crate ) height:      Option<Length>,
	/// When `true`, focusing the field selects the whole value so the
	/// next keystroke replaces it. Standard behaviour for numeric
	/// pickers and short-form fields where the user usually wants to
	/// retype rather than edit. Default `false` — long-form fields
	/// keep the cursor at the end on focus.
	pub( crate ) select_on_focus: bool,
	/// Self-managed "show / hide password" eye affordance — when
	/// `Some( ( visible, on_toggle ) )` the field renders an
	/// `actions/visible` ↔ `actions/invisible` icon at its right
	/// edge, taps on that icon dispatch `on_toggle` instead of
	/// placing the cursor, and the bullet substitution flips with
	/// `visible` (overriding `secure`). Set on a field that already
	/// has `secure( true )` and the explicit flag becomes redundant
	/// — the toggle controls the visibility from then on.
	pub( crate ) password_toggle: Option<( bool, Msg )>,
	/// When `true`, the field renders its box and value but takes no keyboard
	/// focus and accepts no input — a read-only display styled as a text field.
	pub( crate ) read_only:   bool,
}

impl<Msg: Clone> TextEdit<Msg>
{
	/// Create a text field with the given placeholder and initial value.
	///
	/// The cursor is placed at the end of the initial value.
	pub fn new( placeholder: String, value: String ) -> Self
	{
		let cursor_pos = value.len();
		Self
		{
			placeholder,
			value,
			on_change:       None,
			on_submit:       None,
			secure:          false,
			multiline:       false,
			rows:            theme::ROWS_DEFAULT,
			cursor_pos,
			id:              None,
			cursor:          None,
			align:           super::text::TextAlign::Left,
			borderless:      false,
			fixed_width:     None,
			font_size:       None,
			height:          None,
			select_on_focus: false,
			password_toggle: None,
			read_only:       false,
		}
	}

	/// Add a "show / hide password" eye toggle pinned to the right
	/// edge of the field. `visible` controls whether the value
	/// renders as bullets (`false`) or plain text (`true`); a tap on
	/// the icon emits `on_toggle` so the caller can flip its own
	/// `bool` state and re-render. Works with or without an explicit
	/// [`Self::secure`] — when this is set, the toggle's `visible`
	/// drives the bullet substitution and the `secure` field is
	/// ignored.
	pub fn password_toggle( mut self, visible: bool, on_toggle: Msg ) -> Self
	{
		self.password_toggle = Some( ( visible, on_toggle ) );
		self
	}

	/// Effective secure flag honoured by drawing / measurement /
	/// hit-testing — [`Self::password_toggle`] takes precedence over
	/// the manual [`Self::secure`] when both are set.
	pub fn effective_secure( &self ) -> bool
	{
		match &self.password_toggle
		{
			Some( ( visible, _ ) ) => !visible,
			None                   => self.secure,
		}
	}

	/// Set the label font size. Accepts logical `f32` pixels or any
	/// [`Length`] (e.g. `Length::vmin( 4.5 ).clamp( 16.0, 32.0 )` to scale
	/// with the surface), resolved in font space exactly like the button
	/// label. Without any font-size call the size follows the process
	/// [`crate::WidgetScaling`] mode at the theme default. Ignored in
	/// multiline mode.
	pub fn font_size( mut self, size: impl Into<Length> ) -> Self
	{
		self.font_size = Some( size.into() );
		self
	}

	/// Set a font size that follows the process [`crate::WidgetScaling`]
	/// mode at the given design px — the fluid counterpart of
	/// [`Self::font_size`]. Shorthand for `.font_size( Length::fluid( n ) )`.
	pub fn font_size_fluid( mut self, design_px: f32 ) -> Self
	{
		self.font_size = Some( Length::fluid( design_px.max( 1.0 ) ) );
		self
	}

	/// Resolve the single-line font size against the widget-scaling mode.
	pub( crate ) fn effective_font_size( &self, canvas: &Canvas ) -> f32
	{
		resolve_font_size( canvas, self.font_size )
	}

	/// Set the single-line field height. Accepts logical `f32` pixels or any
	/// [`Length`] (e.g. `Length::vmin( 9.0 ).clamp( 44.0, 72.0 )` to scale
	/// the box with the surface, or to match a fluid button). Ignored in
	/// multiline mode, where the height follows [`Self::rows`].
	pub fn height( mut self, h: impl Into<Length> ) -> Self
	{
		self.height = Some( h.into() );
		self
	}

	/// Select the whole value when the field receives focus, so the
	/// next keystroke replaces it. Default `false`.
	pub fn select_on_focus( mut self, on: bool ) -> Self
	{
		self.select_on_focus = on;
		self
	}

	/// Set the horizontal alignment of the displayed text. Default
	/// [`TextAlign::Left`](super::text::TextAlign::Left).
	pub fn align( mut self, a: super::text::TextAlign ) -> Self
	{
		self.align = a;
		self
	}

	/// Skip the field's background fill and border stroke — useful
	/// when the field is nested inside a container that already
	/// paints its own surface.
	pub fn borderless( mut self, on: bool ) -> Self
	{
		self.borderless = on;
		self
	}

	/// Override the preferred width reported to the parent layout.
	/// Accepts logical `f32` pixels or any [`Length`] — pair a
	/// [`Length::fluid`] width with a fluid font size so box and glyphs
	/// scale together. Without this the field claims `max_width`.
	pub fn fixed_width( mut self, w: impl Into<Length> ) -> Self
	{
		self.fixed_width = Some( w.into() );
		self
	}

	/// Override the pointer cursor shape shown on hover. Defaults to
	/// [`CursorShape::Text`](crate::CursorShape::Text) (I-beam).
	pub fn cursor( mut self, shape: crate::types::CursorShape ) -> Self
	{
		self.cursor = Some( shape );
		self
	}

	/// Switch to multiline (text-area) mode. The box is laid out with
	/// [`Self::rows`] visible rows of height, line breaks in the value
	/// are rendered as separate rows, and Enter inserts a `\n` instead
	/// of firing [`Self::on_submit`]. Ignored when [`Self::secure`] is
	/// `true`.
	pub fn multiline( mut self, m: bool ) -> Self
	{
		self.multiline = m;
		self
	}

	/// Configure the number of visible rows in multiline mode. Defaults
	/// to 5; ignored when [`Self::multiline`] is `false`.
	pub fn rows( mut self, n: u32 ) -> Self
	{
		self.rows = n.max( 1 );
		self
	}

	/// Set the callback invoked on every keystroke with the updated value.
	pub fn on_change( mut self, f: impl Fn(String) -> Msg + 'static ) -> Self
	{
		self.on_change = Some( Arc::new( f ) );
		self
	}

	/// Set the message emitted when Enter is pressed.
	pub fn on_submit( mut self, msg: Msg ) -> Self
	{
		self.on_submit = Some( msg );
		self
	}

	/// Enable or disable password mode.
	///
	/// When `true`, this widget:
	///
	/// 1. Renders the value as bullet characters (`•`) instead of the
	///    raw glyphs.
	/// 2. Forces single-line mode (multiline + secure is mutually
	///    exclusive — passwords don't have line breaks).
	/// 3. Wipes the underlying byte buffer with zero before the
	///    `String` allocation is returned to the allocator. The wipe
	///    runs in `Drop` for both the `TextEdit` itself and for the
	///    per-frame [`crate::widget::WidgetHandlers::TextEdit`] snapshot
	///    the runtime keeps for input dispatch — so the in-tree copies
	///    that ltk owns never linger as plain text in freed memory.
	///
	/// # Threat model — what `secure` covers
	///
	/// Inside the widget tree the runtime keeps two copies of the
	/// value for the lifetime of one frame: the `TextEdit` itself and
	/// the `WidgetHandlers` snapshot. Both run the `secure_zero` wipe
	/// on `Drop`, so when the next frame replaces them (the typical
	/// case — `view()` rebuilds every frame) the freed allocations are
	/// overwritten before being released back to the allocator. The
	/// wipe uses volatile writes + a `compiler_fence` so the optimiser
	/// cannot elide it as dead code (the implementation is in the
	/// crate-private `secure_mem` module).
	///
	/// # What `secure` does **not** cover
	///
	/// * **The application's own state.** The `String` you pass in
	///   through `text_edit( placeholder, &self.password )` lives on
	///   **your** struct, not on the widget. Wiping it is
	///   your job — typically a `Drop` impl on the credential
	///   container, or an explicit
	///   `secure_mem::secure_zero( password.as_bytes_mut() )` after
	///   the auth handshake completes.
	/// * **Callback-allocated copies.** Every keystroke passes through
	///   `on_change( |s: String| ... )`, which receives a fresh
	///   `String` clone. If your closure stores or forwards that
	///   `String` (e.g. clone it into a worker thread for PAM), each
	///   stored copy is the consumer's responsibility to wipe. ltk
	///   only owns the buffers it allocated itself.
	/// * **OS-level disclosure surfaces.** Swap-out, hibernation
	///   images, and core dumps are outside any user-space wipe's
	///   reach. For threat models that require resistance to these,
	///   compile against an `mlock`-aware allocator, disable swap on
	///   the credential mount, and restrict core-dump capability with
	///   `prctl( PR_SET_DUMPABLE, 0 )` on the process.
	/// * **Compositor-side records.** Wayland text-input protocols can
	///   surface preedit / commit strings to the compositor's IME stack.
	///   `secure` does *not* suppress text-input-v3 — the field still
	///   registers so the on-screen keyboard activates on it — but it is
	///   flagged `Password` with `SensitiveData | HiddenText`, asking the
	///   IME / OSK to skip prediction, autocorrect and storing the value.
	///   The value still reaches the (trusted) compositor/IME; for a
	///   stricter threat model, suppress text-input on secure fields
	///   instead (at the cost of losing the OSK there).
	///
	/// See the in-repo `SECURITY.md` for the full threat-model write-up
	/// (the *Hardening features* section enumerates each guarantee and
	/// its boundary).
	pub fn secure( mut self, s: bool ) -> Self
	{
		self.secure = s;
		self
	}

	/// Render the field read-only: shows the value styled as a field but
	/// takes no focus and accepts no input.
	pub fn read_only( mut self, on: bool ) -> Self
	{
		self.read_only = on;
		self
	}

	/// Assign a stable identifier for focus management.
	pub fn id( mut self, id: WidgetId ) -> Self
	{
		self.id = Some( id );
		self
	}

	/// Return the preferred `(width, height)` given available `max_width`.
	///
	/// Single-line: theme-defined `HEIGHT`.
	/// Multiline: enough room for [`Self::rows`] lines plus padding.
	pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
	{
		if self.multiline && !self.effective_secure()
		{
			let line_h = theme::FONT_SIZE * theme::LINE_H_MULT;
			let h      = self.rows as f32 * line_h + theme::PAD_V_MULTI * 2.0;
			( max_width, h )
		} else {
			let w = self.fixed_width
				.map( |l| canvas.resolve_geom( l ).min( max_width ) )
				.unwrap_or( max_width );
			let h = self.height
				.map( |l| canvas.resolve_geom( l ) )
				.unwrap_or_else( || canvas.geom_px( theme::HEIGHT ) );
			( w, h )
		}
	}

	/// `true` when the widget is laid out as a multi-row text area —
	/// i.e. [`Self::multiline`] was set and [`Self::effective_secure`]
	/// is `false`. A `password_toggle` field collapses to single-line
	/// like an explicit `secure( true )` does.
	pub fn is_multiline( &self ) -> bool
	{
		self.multiline && !self.effective_secure()
	}

	/// Translate a pointer position inside `rect` to the byte offset
	/// in the field's `value` that the cursor should land on. Thin
	/// wrapper around `byte_offset_at` using this widget's value /
	/// flags.
	pub fn byte_offset_at_self(
		&self,
		canvas:     &Canvas,
		rect:       Rect,
		pos:        crate::types::Point,
		cursor_pos: usize,
	) -> usize
	{
		byte_offset_at(
			canvas, rect, pos, &self.value, self.is_multiline(), self.effective_secure(), cursor_pos, self.align, self.font_size,
		)
	}

	/// Border stroke is centered on `rect`, so half the stroke width plus ~1 px
	/// of antialiasing bleed sits outside. The widest stroke is the focused
	/// border, so use that as the envelope.
	pub fn paint_bounds( &self, rect: Rect ) -> Rect
	{
		rect.expand( theme::FOCUS_BORDER_W * 0.5 + 1.0 )
	}

	/// Return the display string — bullet characters in secure mode, plain value otherwise.
	pub fn display_text( &self ) -> String
	{
		if self.effective_secure()
		{
			"\u{2022}".repeat( self.value.chars().count() )
		} else {
			self.value.clone()
		}
	}

	/// Wrap this widget in an [`Element`].
	pub fn into_element( self ) -> Element<Msg>
	{
		Element::TextEdit( self )
	}

	/// Insert a string at the current cursor position, advance the cursor, and
	/// return the `on_change` message if one is set.
	pub fn insert_str( &mut self, s: &str ) -> Option<Msg>
	{
		self.value.insert_str( self.cursor_pos.min( self.value.len() ), s );
		self.cursor_pos = (self.cursor_pos + s.len()).min( self.value.len() );
		self.on_change.as_ref().map( |f| f( self.value.clone() ) )
	}

	/// Delete the character before the cursor and return the `on_change` message
	/// if one is set. Does nothing if the cursor is already at position 0.
	pub fn backspace( &mut self ) -> Option<Msg>
	{
		if self.cursor_pos == 0 { return None; }
		let chars: Vec<char> = self.value.chars().collect();
		let char_pos = self.value[..self.cursor_pos].chars().count();
		if char_pos == 0 { return None; }
		let mut chars = chars;
		let removed = chars.remove( char_pos - 1 );
		self.cursor_pos -= removed.len_utf8();
		self.value = chars.iter().collect();
		self.on_change.as_ref().map( |f| f( self.value.clone() ) )
	}

	pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> TextEdit<U>
	where
		U: Clone + 'static,
		Msg: 'static,
	{
		// Wrap on_change the same way the slider does. on_submit is a
		// plain `Option<Msg>`, so it goes through the user mapper once.
		let on_change = self.on_change.clone().map( |old| -> Arc<dyn Fn( String ) -> U>
		{
			let mapper = Arc::clone( f );
			Arc::new( move |s| ( *mapper )( ( *old )( s ) ) )
		} );
		TextEdit
		{
			placeholder:     self.placeholder.clone(),
			value:           self.value.clone(),
			on_change,
			on_submit:       self.on_submit.clone().map( |m| ( *f )( m ) ),
			secure:          self.secure,
			multiline:       self.multiline,
			rows:            self.rows,
			cursor_pos:      self.cursor_pos,
			id:              self.id,
			cursor:          self.cursor,
			align:           self.align,
			borderless:      self.borderless,
			fixed_width:     self.fixed_width,
			font_size:       self.font_size,
			height:          self.height,
			select_on_focus: self.select_on_focus,
			password_toggle: self.password_toggle.clone().map( |( v, m )| ( v, ( *f )( m ) ) ),
			read_only:       self.read_only,
		}
	}
}

impl<Msg: Clone> Drop for TextEdit<Msg>
{
	/// When `secure( true )` is set, scrub the value bytes before the
	/// underlying `String` allocation is returned to the allocator. The
	/// non-secure path is a no-op so the cost is paid only by widgets
	/// that opted into credential handling.
	fn drop( &mut self )
	{
		if self.secure || self.password_toggle.is_some()
		{
			// SAFETY: as_mut_vec exposes the underlying byte buffer of the
			// String. We only overwrite each byte with zero, which is valid
			// UTF-8 (a sequence of NUL codepoints), so the String invariant
			// is preserved through to the Vec drop that runs immediately
			// after this fn returns.
			let bytes = unsafe { self.value.as_mut_vec() };
			secure_zero( bytes );
		}
	}
}
