//! `cargo run --example mini_shell`
//!
//! A self-contained mini-shell that exercises the patterns described in
//! `docs/architecture.md`:
//!
//! - Multi-screen routing on the main surface (Home / Settings)
//! - Two coordinated overlays (Quick Settings + Confirm dialog)
//! - A pass-through OSD overlay with a fade-in/fade-out animation driven by
//!   `is_animating()`
//! - Live dark/light theme switching from a UI toggle
//! - Nested message enums (`AppMsg::Home(HomeMsg)`, `AppMsg::Settings(...)`)
//! - One module per screen / overlay so the file scales like a real app
//!
//! Controls:
//!   Esc          — quit
//!   Swipe down   — open Quick Settings
//!   Tap outside  — dismiss any overlay
//!
//! NOTE: ltk is a Wayland toolkit. Run inside a Wayland session
//! (sway, labwc, the Liberux desktop, …).

use std::time::{ Duration, Instant };

use ltk::
{
	App, ButtonVariant, Color, Element, Keysym, Length, OverlayId, OverlaySpec,
	ThemeMode,
	button, column, container, progress_bar, row, slider, spacer, text, toggle,
};

// ── Stable overlay ids ────────────────────────────────────────────────────────
//
// Declare ids as constants so the runtime can diff the overlay list across
// frames and keep the underlying surfaces alive.

const OVERLAY_QUICK_SETTINGS: OverlayId = OverlayId( 1 );
const OVERLAY_CONFIRM:        OverlayId = OverlayId( 2 );
const OVERLAY_TOAST:          OverlayId = OverlayId( 3 );

const TOAST_LIFETIME: Duration = Duration::from_millis( 1800 );
const TOAST_FADE_IN:  Duration = Duration::from_millis( 200 );
const TOAST_FADE_OUT: Duration = Duration::from_millis( 600 );

// ── Routing ───────────────────────────────────────────────────────────────────

#[ derive( Clone, Copy, PartialEq, Eq ) ]
enum Route { Home, Settings }

// ── Confirm dialog payload ────────────────────────────────────────────────────
//
// Models a user-initiated action that needs confirmation before it takes
// effect. The dialog itself is just a view of `AppState::pending_action`.

#[ derive( Clone, Copy ) ]
enum Action
{
	PowerOff,
	Reset,
}

impl Action
{
	fn label( self ) -> &'static str
	{
		match self
		{
			Action::PowerOff => "Power off this device?",
			Action::Reset    => "Reset all settings to defaults?",
		}
	}
}

// ── Message tree ──────────────────────────────────────────────────────────────
//
// The top-level enum stays small and forwards to per-screen sub-enums — the
// recommended pattern once a shell has more than a handful of messages.

#[ derive( Clone ) ]
enum AppMsg
{
	Nav( Route ),
	Home( HomeMsg ),
	Settings( SettingsMsg ),

	// Overlay coordination
	OpenQuickSettings,
	CloseQuickSettings,
	SetBrightness( f32 ),

	ShowConfirm( Action ),
	DismissConfirm,
	ConfirmYes,

	// Cross-cutting
	SetThemeMode( ThemeMode ),
	ShowToast( String ),
	Quit,
}

#[ derive( Clone ) ]
enum HomeMsg     { LaunchApp( &'static str ) }
#[ derive( Clone ) ]
enum SettingsMsg { ToggleNotifications }

// ── App state ─────────────────────────────────────────────────────────────────

struct AppState
{
	route: Route,

	// Per-screen state (in a real app each lives in its own module struct).
	home:     home::Home,
	settings: settings::Settings,

	// Overlay state
	quick_settings_visible: bool,
	pending_action:         Option<Action>,
	brightness:             f32,

	// Animated OSD toast
	toast_text:    Option<String>,
	toast_started: Option<Instant>,
}

impl AppState
{
	fn new() -> Self
	{
		Self
		{
			route:                   Route::Home,
			home:                    home::Home::new(),
			settings:                settings::Settings::new(),
			quick_settings_visible:  false,
			pending_action:          None,
			brightness:              0.7,
			toast_text:              None,
			toast_started:           None,
		}
	}

	fn show_toast( &mut self, msg: String )
	{
		self.toast_text    = Some( msg );
		self.toast_started = Some( Instant::now() );
	}

	/// 0.0 = invisible, 1.0 = fully opaque. Used by the OSD overlay's view.
	fn toast_alpha( &self ) -> f32
	{
		let Some( start ) = self.toast_started else { return 0.0 };
		let elapsed = start.elapsed();
		if elapsed < TOAST_FADE_IN
		{
			elapsed.as_secs_f32() / TOAST_FADE_IN.as_secs_f32()
		} else if elapsed < TOAST_LIFETIME - TOAST_FADE_OUT {
			1.0
		} else if elapsed < TOAST_LIFETIME {
			let t = ( TOAST_LIFETIME - elapsed ).as_secs_f32() / TOAST_FADE_OUT.as_secs_f32();
			t.max( 0.0 )
		} else {
			0.0
		}
	}
}

// ── App trait ─────────────────────────────────────────────────────────────────

impl App for AppState
{
	type Message = AppMsg;

	fn view( &self ) -> Element<AppMsg>
	{
		match self.route
		{
			Route::Home     => self.home.view(),
			Route::Settings => self.settings.view(),
		}
	}

	fn overlays( &self ) -> Vec<OverlaySpec<AppMsg>>
	{
		let mut out = Vec::new();

		if self.quick_settings_visible
		{
			out.push( OverlaySpec
			{
				id:                 OVERLAY_QUICK_SETTINGS,
				layer:              ltk::Layer::Overlay,
				anchor:             ltk::Anchor::ALL,
				size:               ( Length::px( 0.0 ), Length::px( 0.0 ) ),
				exclusive_zone:     0,
				keyboard_exclusive: false,
				input_region:       None,
				view:               overlays::quick_settings( self ),
				on_dismiss:         Some( AppMsg::CloseQuickSettings ),
				anchor_widget_id:   None,
			} );
		}

		if let Some( action ) = self.pending_action
		{
			out.push( OverlaySpec
			{
				id:                 OVERLAY_CONFIRM,
				layer:              ltk::Layer::Overlay,
				anchor:             ltk::Anchor::ALL,
				size:               ( Length::px( 0.0 ), Length::px( 0.0 ) ),
				exclusive_zone:     0,
				keyboard_exclusive: false,
				input_region:       None,
				view:               overlays::confirm( action ),
				on_dismiss:         Some( AppMsg::DismissConfirm ),
				anchor_widget_id:   None,
			} );
		}

		if self.toast_text.is_some()
		{
			out.push( OverlaySpec
			{
				id:                 OVERLAY_TOAST,
				layer:              ltk::Layer::Overlay,
				anchor:             ltk::Anchor::ALL,
				size:               ( Length::px( 0.0 ), Length::px( 0.0 ) ),
				exclusive_zone:     0,
				keyboard_exclusive: false,
				// Pass-through: the OSD must not steal taps from anything below.
				input_region:       Some( Vec::new() ),
				view:               overlays::toast( self ),
				on_dismiss:         None,
				anchor_widget_id:   None,
			} );
		}

		out
	}

	fn update( &mut self, msg: AppMsg )
	{
		match msg
		{
			AppMsg::Nav( r )           => self.route = r,
			AppMsg::Home( m )          =>
			{
				if let Some( follow ) = self.home.update( m )
				{
					self.update( follow );
				}
			}
			AppMsg::Settings( m )      => self.settings.update( m ),

			AppMsg::OpenQuickSettings  => self.quick_settings_visible = true,
			AppMsg::CloseQuickSettings => self.quick_settings_visible = false,
			AppMsg::SetBrightness( v ) => self.brightness = v,

			AppMsg::ShowConfirm( a )   => self.pending_action = Some( a ),
			AppMsg::DismissConfirm     => self.pending_action = None,
			AppMsg::ConfirmYes         =>
			{
				if let Some( action ) = self.pending_action.take()
				{
					match action
					{
						Action::PowerOff => self.show_toast( "Pretending to power off…".into() ),
						Action::Reset    =>
						{
							self.settings = settings::Settings::new();
							self.brightness = 0.7;
							self.show_toast( "Settings reset".into() );
						}
					}
				}
			}

			AppMsg::SetThemeMode( m )  => ltk::set_active_mode( m ),
			AppMsg::ShowToast( s )     => self.show_toast( s ),
			AppMsg::Quit               => std::process::exit( 0 ),
		}
	}

	// Animation: keep redrawing while the toast is on screen so it can fade.
	fn is_animating( &self ) -> bool
	{
		self.toast_started.is_some()
	}

	// Auto-expire the toast once its lifetime is up. Putting this in
	// `poll_external` (rather than `update`) means the loop wakes itself when
	// `is_animating()` is driving redraws and cleans up without user input.
	fn poll_external( &mut self ) -> Vec<AppMsg>
	{
		if let Some( start ) = self.toast_started
		{
			if start.elapsed() >= TOAST_LIFETIME
			{
				self.toast_text    = None;
				self.toast_started = None;
			}
		}
		Vec::new()
	}

	fn on_swipe_down( &mut self ) -> Option<AppMsg>
	{
		Some( AppMsg::OpenQuickSettings )
	}

	fn swipe_down_threshold( &self ) -> f32 { 0.10 }

	fn on_key( &mut self, k: Keysym ) -> Option<AppMsg>
	{
		match k
		{
			Keysym::Escape => Some( AppMsg::Quit ),
			_              => None,
		}
	}

	fn background_color( &self ) -> Color
	{
		// Use the active theme's background so dark/light flips immediately.
		ltk::theme_palette().bg
	}
}

// ── Per-screen modules ────────────────────────────────────────────────────────
//
// In a real app each of these lives in its own file (`src/home.rs`,
// `src/settings.rs`, …). Inlined here so the example stays a single file.

mod home
{
	use super::*;

	pub struct Home
	{
		pub last_launched: Option<&'static str>,
	}

	impl Home
	{
		pub fn new() -> Self { Self { last_launched: None } }

		/// Cmd-style: returns an optional follow-up message instead of mutating
		/// shared state. The parent `update` runs the follow-up, which keeps
		/// the borrow checker happy.
		pub fn update( &mut self, msg: HomeMsg ) -> Option<AppMsg>
		{
			match msg
			{
				HomeMsg::LaunchApp( name ) =>
				{
					self.last_launched = Some( name );
					Some( AppMsg::ShowToast( format!( "Launched {}", name ) ) )
				}
			}
		}

		pub fn view( &self ) -> Element<AppMsg>
		{
			let palette = ltk::theme_palette();

			let title = text( "Mini Shell" )
				.size( 28.0 )
				.color( palette.text_primary )
				.align_center();

			let status = text( match self.last_launched
				{
					Some( n ) => format!( "Last launched: {}", n ),
					None      => "No app launched yet".into(),
				} )
				.size( 13.0 )
				.color( palette.text_secondary )
				.align_center();

			let mut grid = ltk::grid::<AppMsg>( 2 ).spacing( 12.0 ).padding( 0.0 );
			for name in &["Browser", "Mail", "Camera", "Music"]
			{
				grid = grid.push(
					button::<AppMsg>( *name )
						.variant( ButtonVariant::Secondary )
						.on_press( AppMsg::Home( HomeMsg::LaunchApp( name ) ) ),
				);
			}

			column::<AppMsg>()
				.padding( 32.0 )
				.spacing( 16.0 )
				.push( title )
				.push( status )
				.push( spacer() )
				.push( grid )
				.push( spacer() )
				.push(
					button::<AppMsg>( "Settings" )
						.on_press( AppMsg::Nav( Route::Settings ) ),
				)
				.into()
		}
	}
}

mod settings
{
	use super::*;

	pub struct Settings
	{
		pub notifications_enabled: bool,
	}

	impl Settings
	{
		pub fn new() -> Self { Self { notifications_enabled: true } }

		pub fn update( &mut self, msg: SettingsMsg )
		{
			match msg
			{
				SettingsMsg::ToggleNotifications => self.notifications_enabled = !self.notifications_enabled,
			}
		}

		pub fn view( &self ) -> Element<AppMsg>
		{
			let palette = ltk::theme_palette();
			let is_dark = ltk::active_mode() == ThemeMode::Dark;
			let next    = if is_dark { ThemeMode::Light } else { ThemeMode::Dark };
			let label   = if is_dark { "Switch to Light" } else { "Switch to Dark" };

			column::<AppMsg>()
				.padding( 32.0 )
				.spacing( 16.0 )
				.push( text( "Settings" ).size( 24.0 ).color( palette.text_primary ) )
				.push(
					toggle::<AppMsg>( self.notifications_enabled )
						.label( "Notifications" )
						.on_toggle( AppMsg::Settings( SettingsMsg::ToggleNotifications ) ),
				)
				.push( spacer() )
				.push( text( "Theme" ).size( 14.0 ).color( palette.text_secondary ) )
				.push(
					button::<AppMsg>( label )
						.variant( ButtonVariant::Secondary )
						.on_press( AppMsg::SetThemeMode( next ) ),
				)
				.push( spacer() )
				.push(
					row::<AppMsg>()
						.spacing( 12.0 )
						.push(
							button::<AppMsg>( "Show toast" )
								.variant( ButtonVariant::Secondary )
								.on_press( AppMsg::ShowToast( "Hello from Settings".into() ) ),
						)
						.push(
							button::<AppMsg>( "Reset settings" )
								.variant( ButtonVariant::Secondary )
								.on_press( AppMsg::ShowConfirm( Action::Reset ) ),
						),
				)
				.push(
					button::<AppMsg>( "Power off" )
						.on_press( AppMsg::ShowConfirm( Action::PowerOff ) ),
				)
				.push( spacer() )
				.push(
					button::<AppMsg>( "Back" )
						.variant( ButtonVariant::Tertiary )
						.on_press( AppMsg::Nav( Route::Home ) ),
				)
				.into()
		}
	}
}

mod overlays
{
	use super::*;

	/// Quick Settings: brightness slider + theme toggle + close button.
	pub fn quick_settings( app: &AppState ) -> Element<AppMsg>
	{
		let palette = ltk::theme_palette();
		let is_dark = ltk::active_mode() == ThemeMode::Dark;
		let next    = if is_dark { ThemeMode::Light } else { ThemeMode::Dark };

		let panel = container::<AppMsg>(
			column::<AppMsg>()
				.padding( 0.0 )
				.spacing( 12.0 )
				.push(
					row::<AppMsg>()
						.spacing( 8.0 )
						.push( text( "Quick Settings" ).size( 18.0 ).color( palette.text_primary ) )
						.push( spacer() )
						.push(
							button::<AppMsg>( "Close" )
								.variant( ButtonVariant::Tertiary )
								.on_press( AppMsg::CloseQuickSettings ),
						),
				)
				.push( text( format!( "Brightness {:.0}%", app.brightness * 100.0 ) )
					.size( 13.0 )
					.color( palette.text_primary ) )
				.push( slider::<AppMsg>( app.brightness ).on_change( AppMsg::SetBrightness ) )
				.push(
					button::<AppMsg>( if is_dark { "Light theme" } else { "Dark theme" } )
						.variant( ButtonVariant::Secondary )
						.on_press( AppMsg::SetThemeMode( next ) ),
				),
		)
		.background( palette.surface_alt )
		.radius( 16.0 )
		.padding( 20.0 );

		// Center the panel on the overlay surface using spacers.
		column::<AppMsg>()
			.padding( 24.0 )
			.spacing( 0.0 )
			.max_width( 360.0 )
			.push( panel )
			.push( spacer() )
			.into()
	}

	/// Confirm dialog. Same overlay pattern, different content.
	pub fn confirm( action: Action ) -> Element<AppMsg>
	{
		let palette = ltk::theme_palette();

		let panel = container::<AppMsg>(
			column::<AppMsg>()
				.padding( 0.0 )
				.spacing( 16.0 )
				.push( text( action.label() )
					.size( 16.0 )
					.color( palette.text_primary )
					.align_center() )
				.push(
					row::<AppMsg>()
						.spacing( 12.0 )
						.push(
							button::<AppMsg>( "Cancel" )
								.variant( ButtonVariant::Tertiary )
								.on_press( AppMsg::DismissConfirm ),
						)
						.push( spacer() )
						.push(
							button::<AppMsg>( "Confirm" )
								.on_press( AppMsg::ConfirmYes ),
						),
				),
		)
		.background( palette.surface_alt )
		.radius( 16.0 )
		.padding( 24.0 );

		column::<AppMsg>()
			.padding( 0.0 )
			.spacing( 0.0 )
			.max_width( 320.0 )
			.push( spacer() )
			.push( panel )
			.push( spacer() )
			.into()
	}

	/// Pass-through OSD. The fade is computed from elapsed time, not stored.
	pub fn toast( app: &AppState ) -> Element<AppMsg>
	{
		let palette = ltk::theme_palette();
		let alpha   = app.toast_alpha();
		let txt     = app.toast_text.as_deref().unwrap_or( "" );

		// Apply the fade by attenuating the surface alpha. Text/progress stay
		// readable; the container background fades.
		let mut bg = palette.surface_alt;
		bg.a *= alpha;

		let mut fg = palette.text_primary;
		fg.a *= alpha;

		let panel = container::<AppMsg>(
			column::<AppMsg>()
				.padding( 0.0 )
				.spacing( 8.0 )
				.push( text( txt ).size( 15.0 ).color( fg ).align_center() )
				.push( progress_bar( alpha ) ),
		)
		.background( bg )
		.radius( 12.0 )
		.padding( 16.0 );

		column::<AppMsg>()
			.padding( 0.0 )
			.spacing( 0.0 )
			.max_width( 280.0 )
			.push( spacer() )
			.push( spacer() )
			.push( spacer() )
			.push( panel )
			.into()
	}
}

// ── Entry point ───────────────────────────────────────────────────────────────

fn main()
{
	// Load the on-disk default theme. ltk has no in-code fallback: the
	// `ltk-theme-default` Debian package (or a dev-time
	// `LTK_THEMES_DIR=<repo>/themes`) must provide
	// `default/theme.json`. `ltk::run` would otherwise panic on first
	// theme access with the same message as the one below.
	let doc = ltk::ThemeDocument::find( "default" )
		.expect( "ltk: the `default` theme is required (install `ltk-theme-default` or set `LTK_THEMES_DIR`)" );
	ltk::set_active_document( doc );
	ltk::set_active_mode( ThemeMode::Dark );

	ltk::run( AppState::new() );
}
