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

//! Toast / Snackbar — short-lived bottom-anchored message overlay.
//!
//! [`Toast`] is **not** an [`Element`]; it is a builder that produces
//! an [`OverlaySpec`] for the application to return from
//! [`App::overlays`](crate::App::overlays). The widget itself is
//! stateless: the application owns the visibility / timer state and
//! returns the overlay only while a toast is pending.
//!
//! Auto-dismissal is the application's responsibility. A typical
//! pattern is to spawn a `calloop` timer (or use the channel sender
//! from [`App::set_channel_sender`](crate::App::set_channel_sender))
//! that fires a "toast expired" message after [`Toast::duration`]
//! elapses.
//!
//! ```rust,no_run
//! # use ltk::{ toast, OverlaySpec, WidgetId };
//! # #[ derive( Clone ) ] enum Msg {}
//! # struct ToastState { message: String }
//! # struct App { toast: Option<ToastState> }
//! # impl App {
//! fn overlays( &self ) -> Vec<OverlaySpec<Msg>>
//! {
//!     match &self.toast
//!     {
//!         Some( t ) => vec![ toast( &t.message ).id( WidgetId( "toast/main" ) ).overlay() ],
//!         None      => vec![],
//!     }
//! }
//! # }
//! ```

use std::collections::hash_map::DefaultHasher;
use std::hash::{ Hash, Hasher };
use std::time::Duration;

use crate::app::{ Anchor, Layer, OverlayId, OverlaySpec };
use crate::types::{ Color, Length, WidgetId };
use super::Element;

mod theme;

#[ cfg( test ) ]
mod tests;

/// Builder for a transient bottom-anchored notification overlay.
///
/// `Toast` does not own its own timer — the application is responsible
/// for clearing the toast (typically by storing its `message` in an
/// `Option` and resetting it via a delayed message).
pub struct Toast<Msg: Clone>
{
	/// Stable id for the overlay. Used to derive the [`OverlayId`] so
	/// the same toast persists across frames when the message stays the
	/// same.
	pub( crate ) id:        WidgetId,
	/// Body text rendered inside the pill.
	pub( crate ) message:   String,
	/// Display duration. The runtime does **not** consume this — it is
	/// returned through [`Toast::duration_value`] so the app's timer
	/// scheduler can read it back.
	pub( crate ) duration:  Duration,
	/// Optional message fired when the user taps anywhere outside the
	/// pill. Convenient for "tap anywhere to dismiss" behaviour.
	pub( crate ) on_dismiss: Option<Msg>,
	/// Optional override for the pill background colour.
	pub( crate ) bg:        Option<Color>,
	/// Optional override for the body text colour.
	pub( crate ) fg:        Option<Color>,
}

impl<Msg: Clone + 'static> Toast<Msg>
{
	/// Create a toast with the given message text. The default id is
	/// `"toast/default"`; override with [`Self::id`] when more than one
	/// toast variant might coexist.
	pub fn new( message: impl Into<String> ) -> Self
	{
		Self
		{
			id:         WidgetId( "toast/default" ),
			message:    message.into(),
			duration:   Duration::from_secs( 2 ),
			on_dismiss: None,
			bg:         None,
			fg:         None,
		}
	}

	/// Override the stable id used to derive the [`OverlayId`].
	pub fn id( mut self, id: WidgetId ) -> Self
	{
		self.id = id;
		self
	}

	/// Set the display duration. The toast itself does not auto-hide;
	/// this value is stored so the application's timer scheduler can
	/// read it back via [`Self::duration_value`].
	pub fn duration( mut self, secs: f32 ) -> Self
	{
		self.duration = Duration::from_secs_f32( secs.max( 0.0 ) );
		self
	}

	/// Read back the configured duration. Lets the app delegate timer
	/// setup to a helper that does not need to know how the toast was
	/// configured.
	pub fn duration_value( &self ) -> Duration
	{
		self.duration
	}

	/// Set the dismiss message fired on a tap outside the pill.
	pub fn on_dismiss( mut self, msg: Msg ) -> Self
	{
		self.on_dismiss = Some( msg );
		self
	}

	/// Override the pill background colour.
	pub fn background( mut self, c: Color ) -> Self
	{
		self.bg = Some( c );
		self
	}

	/// Override the body text colour.
	pub fn color( mut self, c: Color ) -> Self
	{
		self.fg = Some( c );
		self
	}

	/// Build an [`Element`] that paints the toast pill bottom-anchored
	/// inside its parent rect. The intended pattern is to push it on
	/// top of the main view via a [`Stack`](crate::stack):
	///
	/// ```text
	/// fn view( &self ) -> Element<Msg>
	/// {
	///     let body = column().push( … );
	///     let mut s = stack().push( body );
	///     if self.toast_until.is_some()
	///     {
	///         s = s.push( toast( "Saved" ).view() );
	///     }
	///     s.into()
	/// }
	/// ```
	///
	/// Works on every compositor — no `wlr-layer-shell` needed —
	/// because the pill is just a regular widget tree drawn on top of
	/// the existing canvas. Use [`Self::overlay`] only when you
	/// specifically need the toast to render *above other windows*
	/// (notification-style), which requires `wlr-layer-shell`.
	pub fn view( self ) -> Element<Msg>
	{
		use super::{ container, text };
		use crate::layout::stack::stack;
		use crate::layout::stack::{ HAlign, VAlign };

		let bg = self.bg.unwrap_or_else( theme::bg );
		let fg = self.fg.unwrap_or_else( theme::text );

		// Inline path: the pill lays out in the app's own (screen-sized)
		// surface and the container auto-sizes to the text, so fluid font /
		// padding scale with the screen without any clipping.
		let pill: Element<Msg> = container(
				text( self.message.clone() )
					.size( Length::widget( theme::FONT_SIZE ) )
					.color( fg )
			)
			.background( bg )
			.padding_h( Length::widget( theme::PAD_H ) )
			.padding_v( Length::widget( 12.0 ) )
			.radius( theme::RADIUS )
			.into();

		stack::<Msg>()
			.push_aligned_margin( pill, HAlign::Center, VAlign::Bottom, theme::BOTTOM_MARGIN as f32 )
			.into()
	}

	/// Build the [`OverlaySpec`] the application returns from
	/// [`App::overlays`](crate::App::overlays). The overlay is anchored
	/// to the bottom edge of the screen with a small breathing margin.
	///
	/// Requires the compositor to advertise `wlr-layer-shell`. For a
	/// portable variant that works everywhere — at the cost of being
	/// confined to the application's surface — use [`Self::view`].
	pub fn overlay( self ) -> OverlaySpec<Msg>
	{
		use super::{ container, text };
		use crate::layout::stack::stack;

		let mut hasher = DefaultHasher::new();
		self.id.0.hash( &mut hasher );
		let overlay_id = OverlayId( hasher.finish() as u32 );

		let bg = self.bg.unwrap_or_else( theme::bg );
		let fg = self.fg.unwrap_or_else( theme::text );

		let pill: Element<Msg> = container(
				text( self.message.clone() )
					.size( Length::widget( theme::FONT_SIZE ) )
					.color( fg )
			)
			.background( bg )
			.padding_h( Length::widget( theme::PAD_H ) )
			.padding_v( Length::widget( 12.0 ) )
			.radius( theme::RADIUS )
			.into();

		// Centre horizontally, anchor to the bottom with a small margin.
		let body: Element<Msg> = stack()
			.push_aligned_margin(
				pill,
				crate::layout::stack::HAlign::Center,
				crate::layout::stack::VAlign::Bottom,
				theme::BOTTOM_MARGIN as f32,
			)
			.into();

		OverlaySpec
		{
			id:                 overlay_id,
			layer:              Layer::Overlay,
			anchor:             Anchor::BOTTOM,
			// The surface scales with the display so the fluid pill always fits.
			size:               ( Length::px( 0.0 ), Length::widget( theme::HEIGHT as f32 + theme::BOTTOM_MARGIN as f32 + 24.0 ) ),
			exclusive_zone:     0,
			keyboard_exclusive: false,
			input_region:       None,
			view:               body,
			on_dismiss:         self.on_dismiss,
			anchor_widget_id:   None,
		}
	}
}

/// Create a [`Toast`] with the given message.
pub fn toast<Msg: Clone + 'static>( message: impl Into<String> ) -> Toast<Msg>
{
	Toast::new( message )
}
