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

//! Modal / non-modal centered dialog: a darkened scrim with a card
//! holding a title, an optional subtitle, an optional body, and a row
//! of action buttons.
//!
//! The widget is implemented as a thin builder over existing
//! primitives: at conversion time
//! ([`From<Dialog<Msg>> for Element<Msg>`](Dialog#impl-From%3CDialog%3CMsg%3E%3E-for-Element%3CMsg%3E))
//! it lowers itself to a [`Stack`](crate::layout::stack::Stack) of
//!
//! 1. a full-surface [`Pressable`](crate::widget::pressable::Pressable)
//!    scrim — `swallow=true` so it absorbs every pointer event that
//!    misses the card (so widgets behind the dialog cannot be clicked),
//!    `on_escape=cancel_msg` so the keyboard ESC handler can find it,
//!    and `on_press=dismiss_msg` only when the dialog is non-modal and
//!    a `dismiss_on_scrim` was configured;
//! 2. a centered card backed by a flat opaque fill (`palette.surface`
//!    with alpha forced to 1.0 — themed `surface-card` / similar
//!    Glass surfaces ship as translucent for the rest of the toolkit,
//!    but a confirmation dialog must read against any background, so
//!    the dialog opts out of the Glass chrome). The card wraps a
//!    *card-area* `Pressable( swallow=true )` so the body itself
//!    silently absorbs taps and only clicks strictly outside the card
//!    can dismiss the dialog. Inside the card sits a column with the
//!    title (700-weight, wraps), the subtitle
//!    (`text_secondary`, wraps), the user-supplied body, and a
//!    right-aligned actions row.
//!
//! ## Example
//!
//! ```rust,no_run
//! # use ltk::{ button, dialog, ButtonVariant, Element };
//! # #[ derive( Clone ) ] enum Msg { Cancel, Confirm }
//! # fn _ex() -> Element<Msg> {
//! dialog()
//!     .title( "Delete partition?" )
//!     .subtitle( "This will erase every file on /dev/sda2." )
//!     .cancel( Msg::Cancel )
//!     .action( button::<Msg>( "Cancel" )
//!         .variant( ButtonVariant::Tertiary )
//!         .on_press( Msg::Cancel ) )
//!     .action( button::<Msg>( "Delete" )
//!         .variant( ButtonVariant::Primary )
//!         .on_press( Msg::Confirm ) )
//!     .into()
//! # }
//! ```
//!
//! ## Modality and dismissal
//!
//! `modal` is `true` by default — every pointer event outside the card
//! is silently swallowed and underlying widgets cannot be reached. Set
//! `modal( false )` to let pointer events pass through to the
//! application **except** that you can still wire a
//! [`Dialog::dismiss_on_scrim`] message that fires only when the user
//! taps strictly outside the card. `dismiss_on_scrim` is rejected at
//! build time for a modal dialog (the contract is "modality means no
//! escape", so an "escape via tap-outside" message is contradictory).
//!
//! `Esc` always fires the [`Dialog::cancel`] message — independent of
//! modality. Wire `cancel` to the same message your "Cancel" /
//! "Dismiss" action button uses so keyboard ESC matches the click
//! behaviour.

use crate::layout::column::column;
use crate::layout::row::row;
use crate::layout::spacer::spacer;
use crate::layout::stack::stack;
use crate::types::{ Color, Corners, Length };

use super::container::container;
use super::pressable::pressable;
use super::text;
use super::Element;

#[ cfg( test ) ]
mod tests;

/// Default scrim opacity over the underlying surface.
pub const SCRIM_ALPHA: f32 = 0.45;
/// Default card max-width design size, applied as `Length::fluid` so
/// the card tracks the surface. Override with [`Dialog::max_width`].
pub const DEFAULT_MAX_WIDTH: f32 = 480.0;
/// Default card corner radius.
pub const CARD_RADIUS:    f32 = 16.0;
/// Default card padding (uniform).
pub const CARD_PADDING:   f32 = 24.0;
/// Default vertical gap between the title, subtitle, body, and
/// actions row.
pub const SECTION_GAP:    f32 = 12.0;
/// Default horizontal gap between action buttons.
pub const ACTION_GAP:     f32 = 8.0;
/// Default title font size.
pub const TITLE_SIZE:     f32 = 22.0;
/// Default title weight.
pub const TITLE_WEIGHT:   u16 = 700;
/// Default subtitle font size.
pub const SUBTITLE_SIZE:  f32 = 14.0;

/// A centered confirmation dialog with optional title, subtitle, body
/// and action buttons.
pub struct Dialog<Msg: Clone>
{
	pub( crate ) title:       Option<String>,
	pub( crate ) subtitle:    Option<String>,
	/// User-supplied body element rendered between the subtitle and
	/// the action row. Use this for sliders, lists, spinners or any
	/// other custom content.
	pub( crate ) body:        Option<Box<Element<Msg>>>,
	pub( crate ) actions:     Vec<Element<Msg>>,
	pub( crate ) modal:       bool,
	/// Optional message dispatched when the user taps the scrim
	/// (strictly outside the card). Always `None` when [`Self::modal`]
	/// is `true`. Construction panics if both are set together.
	pub( crate ) dismiss_msg: Option<Msg>,
	/// Optional message dispatched when the user presses `Escape`
	/// while the dialog is on screen. Wire this to the same message
	/// your "Cancel" action button uses.
	pub( crate ) cancel_msg:  Option<Msg>,
	pub( crate ) max_width:   Length,
}

impl<Msg: Clone> Default for Dialog<Msg>
{
	fn default() -> Self
	{
		Self::new()
	}
}

impl<Msg: Clone> Dialog<Msg>
{
	/// Construct a default modal dialog with no title, subtitle,
	/// body, or actions. Build it up with the chained setters.
	pub fn new() -> Self
	{
		Self
		{
			title:       None,
			subtitle:    None,
			body:        None,
			actions:     Vec::new(),
			modal:       true,
			dismiss_msg: None,
			cancel_msg:  None,
			max_width:   Length::fluid( DEFAULT_MAX_WIDTH ),
		}
	}

	/// Set the dialog title. Wraps across multiple lines if it does
	/// not fit on a single one.
	pub fn title( mut self, t: impl Into<String> ) -> Self
	{
		self.title = Some( t.into() );
		self
	}

	/// Set the dialog subtitle. Wraps across multiple lines if it
	/// does not fit on a single one.
	pub fn subtitle( mut self, s: impl Into<String> ) -> Self
	{
		self.subtitle = Some( s.into() );
		self
	}

	/// Replace the dialog body with a custom element — slider,
	/// progress indicator, list, anything. Rendered between the
	/// subtitle and the action row.
	pub fn body( mut self, e: impl Into<Element<Msg>> ) -> Self
	{
		self.body = Some( Box::new( e.into() ) );
		self
	}

	/// Append an action element to the right-aligned action row.
	/// Typically a [`Button`](crate::widget::button::Button); any
	/// `Element` is accepted.
	pub fn action( mut self, e: impl Into<Element<Msg>> ) -> Self
	{
		self.actions.push( e.into() );
		self
	}

	/// Toggle modality. Default: `true` (every pointer event outside
	/// the card is silently absorbed).
	pub fn modal( mut self, on: bool ) -> Self
	{
		self.modal = on;
		self
	}

	/// Bind a message dispatched when the user taps the scrim
	/// (outside the card). Only valid for non-modal dialogs;
	/// construction panics if combined with `modal( true )`.
	pub fn dismiss_on_scrim( mut self, msg: Msg ) -> Self
	{
		self.dismiss_msg = Some( msg );
		self
	}

	/// Bind a message dispatched when the user presses `Escape`.
	/// Wire the same message your "Cancel" / "Dismiss" action button
	/// uses so keyboard and pointer behaviour match.
	pub fn cancel( mut self, msg: Msg ) -> Self
	{
		self.cancel_msg = Some( msg );
		self
	}

	/// Override the card's maximum width. Accepts logical `f32`
	/// pixels or any [`Length`]. Default is `Length::fluid( 480.0 )`,
	/// so the card scales with the same curve as the stock buttons
	/// inside it.
	pub fn max_width( mut self, w: impl Into<Length> ) -> Self
	{
		self.max_width = w.into();
		self
	}

}

impl<Msg: Clone + 'static> From<Dialog<Msg>> for Element<Msg>
{
	fn from( d: Dialog<Msg> ) -> Element<Msg>
	{
		assert!(
			!( d.modal && d.dismiss_msg.is_some() ),
			"dialog: dismiss_on_scrim is not valid when modal=true",
		);

		let palette = crate::theme::palette();

		// 1. Inner card column: title, subtitle, body, actions. The
		// card's interior spacing is CARD_PADDING on the container —
		// zero here, or the column's 16 px default would stack on top
		// of it and steal width from the actions row.
		let mut card_col = column::<Msg>().spacing( SECTION_GAP ).padding( 0.0 );
		if let Some( title ) = d.title
		{
			card_col = card_col.push(
				text( title )
					.size( TITLE_SIZE )
					.weight( TITLE_WEIGHT )
					.color( palette.text_primary )
					.wrap( true ),
			);
		}
		if let Some( subtitle ) = d.subtitle
		{
			card_col = card_col.push(
				text( subtitle )
					.size( SUBTITLE_SIZE )
					.color( palette.text_secondary )
					.wrap( true ),
			);
		}
		if let Some( body ) = d.body
		{
			card_col = card_col.push( *body );
		}
		if !d.actions.is_empty()
		{
			let mut actions_row = row::<Msg>().spacing( ACTION_GAP ).push( spacer() );
			for a in d.actions
			{
				actions_row = actions_row.push( a );
			}
			card_col = card_col.push( actions_row );
		}

		// 2. Card surface — flat opaque fill, no themed surface stack.
		// Themed surfaces (`surface-card` / `surface-dialog`) ship as
		// translucent Glass for the rest of the toolkit; for a
		// confirmation dialog the body must read against any
		// background, so we force `palette.surface` to full opacity
		// and skip the Glass chrome.
		let card_bg = Color { a: 1.0, ..palette.surface };
		let card = container::<Msg>( card_col )
			.background( card_bg )
			.radius( Corners::all( CARD_RADIUS ) )
			.padding( CARD_PADDING );

		// 3. Card-area swallow: a Pressable wrapping the card so
		// taps on the body silently absorb without firing the
		// scrim's `dismiss_msg`. Always armed (modal or not) — the
		// card never dismisses.
		let card_swallow = pressable::<Msg>( card ).swallow( true );

		// 4. Center the card on the screen. The outer column claims
		// the full surface; `center_y` + `align_center_x` keep the
		// card vertically and horizontally centered, and `max_width`
		// caps it at `d.max_width` even on ultra-wide layouts. The
		// explicit padding is the card's minimum margin to the
		// surface edges on narrow windows.
		let centered = column::<Msg>()
			.center_y( true )
			.align_center_x( true )
			.max_width( d.max_width )
			.padding( 16.0 )
			.push( card_swallow );

		// 5. Scrim — a full-bleed Pressable with the dim layer
		// rendered behind it. `swallow=true` means it always shows
		// up in `widget_rects` (modal dialogs need this so missing
		// hits do not fall through to the underlying app); when
		// non-modal AND a `dismiss_msg` was configured, taps fire
		// the message. The cancel-on-ESC binding lives here too.
		let scrim_bg = container::<Msg>( spacer() )
			.background( Color { r: 0.0, g: 0.0, b: 0.0, a: SCRIM_ALPHA } )
			.radius( Corners::ZERO );
		let mut scrim_press = pressable::<Msg>( scrim_bg ).swallow( true );
		if let Some( msg ) = d.dismiss_msg
		{
			scrim_press = scrim_press.on_press( msg );
		}
		if let Some( msg ) = d.cancel_msg
		{
			scrim_press = scrim_press.on_escape( msg );
		}

		// 6. Stack scrim + centered card. Layout walker pushes
		// children in order, so the scrim's `flat_idx` < the card
		// area's < each action button's; `iter().rev()` hit-testing
		// therefore finds buttons first, then the card-area
		// swallow, and finally the scrim outside the card.
		stack::<Msg>()
			.push( scrim_press )
			.push( centered )
			.into()
	}
}

/// Construct a [`Dialog`]. See the type's documentation for the full
/// builder API and the lowering details.
pub fn dialog<Msg: Clone>() -> Dialog<Msg>
{
	Dialog::new()
}
