//! Launcher view assembly.
//!
//! Two outer-chrome branches share the same inner rendering primitives:
//!
//! * Desktop / landscape — left rail of session/launcher actions plus
//!   three sections (Pinned, Recently used, All Apps) stacked
//!   vertically with their own grids. The search box anchors the top
//!   center.
//! * Mobile / portrait — no rail, no sections; the search box spans
//!   the full width and the grid below shows every app at once,
//!   matching the launcher's pre-redesign behaviour.
//!
//! Both branches paint the lockscreen wallpaper opaque (no scrim, no
//! glass on top) and translate the resulting panel via
//! [`Stack::push_translated`] so the whole panel slides up from below
//! the surface as the animation progresses — the inverse of the QS
//! pull-down.
//!
//! An active search query collapses the desktop sections into a single
//! flat grid: the user is looking for one specific app, not browsing
//! categories, so the categorical layout stops paying for itself.

use rust_i18n::t;

use crate::config::Config;
use crate::launcher::icon_cache::IconCache;
use crate::message::Message;

use super::app_grid::AppEntry;
use super::icons::Icons;
use super::search;
use super::AppLauncher;

/// Canonical icon size for the launcher grid cells, in logical pixels
/// (multiplied by `ltk::density()` where used, so the physical size
/// follows the output scale).
const ICON_PX: f32 = 48.0;

/// Physical size the grid decodes app icons at, for the startup prewarm:
/// the landscape and portrait rules of `flat_grid`.
pub( crate ) fn grid_icon_px( screen_width: u32, screen_height: u32 ) -> u32
{
	if screen_width > screen_height
	{
		( ICON_PX * ltk::density().max( 1.0 ) ) as u32
	} else {
		( screen_width as f32 * MOBILE_ICON_FRAC ).round() as u32
	}
}

/// Desktop / landscape grid sizing, in logical px scaled by the output
/// density at the use sites. Horizontal / vertical gaps and the label
/// font size (the font resolves in logical space on its own).
const DESKTOP_SPACING_X: f32 = 12.0;
const DESKTOP_SPACING_Y: f32 = 28.0;
const DESKTOP_LABEL_PX:  f32 = 10.0;

/// Logical px per launcher cell — drives the column-count formula on
/// desktop so the grid scales with screen width.
const CELL_PX: f32 = 110.0;

/// Width of the desktop left rail. Pinned to the screen's left edge,
/// orthogonal to the panel content; sized to the rail icon plus a
/// generous tap margin on either side.
const RAIL_WIDTH: f32 = 56.0;

/// Vertical gap between rail icons.
const RAIL_GAP: f32 = 24.0;

/// Rail icon size in logical px — small monochrome glyph, no
/// background pill.
const RAIL_ICON_PX: f32 = 22.0;

/// Padding around the section column (search bar + grids).
const PANEL_PAD: f32 = 24.0;

/// Maximum width of the search pill on desktop. Phone / tablet
/// portrait ignores the cap and lets the pill stretch to the
/// available width.
const SEARCH_MAX_W: f32 = 520.0;

// Mobile grid sizing (column count + screen-width fractions) is shared
// across the launcher, the homescreen grid and the dock, so the values
// live in one place — `homescreen::geometry`.
use crate::homescreen::geometry::{ MOBILE_COLS, MOBILE_ICON_FRAC, MOBILE_GAP_FRAC, MOBILE_LABEL_FRAC };

/// Public entry point used by `AppLauncher::view`.
pub( super ) fn build(
	launcher:      &AppLauncher,
	config:        &Config,
	icon_cache:    &mut IconCache,
	screen_width:  u32,
	screen_height: u32,
	display_scale: u32,
	on_launch:     impl Fn( String ) -> Message,
	on_drag_start: impl Fn( String ) -> Message,
	on_search:     impl Fn( String ) -> Message + 'static,
) -> ltk::Element<Message>
{
	let palette      = ltk::theme_palette();
	let is_landscape = screen_width > screen_height;

	let visible_apps: Vec<AppEntry> = if launcher.search_query.is_empty()
	{
		launcher.apps.clone()
	}
	else
	{
		search::search_apps( &launcher.apps, &launcher.search_query )
	};

	let inner_content = if is_landscape && launcher.search_query.is_empty()
	{
		desktop_sections(
			launcher, config, &visible_apps,
			icon_cache, screen_width, &palette,
			&on_launch, &on_drag_start,
		)
	}
	else
	{
		flat_grid(
			launcher, &visible_apps,
			icon_cache, screen_width, is_landscape,
			&palette, &on_launch, &on_drag_start,
		)
	};

	let d = ltk::density().max( 1.0 );
	let search_pill = build_search_pill( launcher, &palette, on_search );
	let search_row: ltk::Element<Message> = if is_landscape
	{
		// Desktop: cap the pill at `SEARCH_MAX_W` and centre it
		// horizontally above the section column. `column.max_width +
		// align_center_x` claims the parent's full width but lays out
		// children inside the capped band.
		ltk::column::<Message>()
			.padding( 0.0 )
			.spacing( 0.0 )
			.max_width( SEARCH_MAX_W * d )
			.align_center_x( true )
			.push( search_pill )
			.into()
	}
	else
	{
		// Mobile: full-width pill, no centring constraint.
		search_pill.into()
	};

	// Pad the column manually so the desktop branch can reserve space
	// on the left for the overlaid rail without re-wrapping. Padding
	// `0` on the column itself keeps the inner `Scroll` widget able to
	// claim every leftover vertical pixel — wrapping the column in a
	// `Row::push(flex(col))` instead would give the column only its
	// natural height (search pill + zero from scroll) and the grid
	// would never get any rect to lay out into. Same reason QS keeps
	// its panel as a column rather than a row of flex columns.
	let mobile_pad = ( screen_width as f32 * MOBILE_GAP_FRAC ).round();
	let pad_left  = if is_landscape { ( RAIL_WIDTH + PANEL_PAD ) * d } else { mobile_pad };
	let pad_right = if is_landscape { PANEL_PAD * d } else { mobile_pad };
	let content_col = ltk::column::<Message>()
		.padding( 0.0 )
		.spacing( 0.0 )
		.push( search_row )
		.push( ltk::spacer().height( 64.0 * d ) )
		.push( ltk::scroll( inner_content ) );

	let content_padded: ltk::Element<Message> = ltk::container::<Message>( content_col )
		.padding_top( PANEL_PAD * d )
		.padding_bottom( PANEL_PAD * d )
		.padding_left( pad_left )
		.padding_right( pad_right )
		.into();

	let inner: ltk::Element<Message> = if is_landscape
	{
		// Stack the content full-width with the rail overlaid at the
		// left edge. The rail is given `VAlign::Fill` so its inner
		// flex spacer can resolve against the surface height (pushing
		// the launcher-toggle icon to the bottom). The content column
		// already pads itself away from the rail's column via
		// `pad_left`.
		ltk::stack::<Message>()
			.push( content_padded )
			.push_aligned(
				rail_column( launcher, &palette ),
				ltk::HAlign::Start,
				ltk::VAlign::Fill,
			)
			.into()
	}
	else
	{
		// Mobile drops the rail entirely.
		content_padded
	};

	// Background: the active mode's lockscreen wallpaper, painted
	// opaque (no transparent scrim, no glass tint) so the homescreen
	// never bleeds through. The bundle handles orientation-aware
	// cropping internally.
	let surface_h = launcher.max_height( screen_height, display_scale );
	let ( bg_rgba, bg_w, bg_h ) = launcher.wallpaper().for_size( screen_width, surface_h );
	let bg = ltk::img_widget( bg_rgba, bg_w, bg_h ).cover();

	// Opaque panel filling the strip. The slide is no longer an in-content
	// translation: the host overlay paints this at rest, and during the
	// slide the same panel rides a subsurface the compositor repositions,
	// so the view always renders flush.
	ltk::stack::<Message>()
		.push( bg )
		.push( inner )
		.into()
}

// =============================================================================
// Search bar
// =============================================================================

/// Pill-shaped search box with a magnifier glyph on the right edge.
/// `text_edit` is wrapped in a row alongside the icon so the row owns
/// both children's horizontal layout — the icon claims its intrinsic
/// width, the text field flexes to fill the rest.
fn build_search_pill(
	launcher:  &AppLauncher,
	palette:   &ltk::Palette,
	on_search: impl Fn( String ) -> Message + 'static,
) -> ltk::Element<Message>
{
	let placeholder = t!( "launcher_search_placeholder" ).to_string();
	let value       = launcher.search_query.clone();

	let field: ltk::Element<Message> = ltk::text_edit::<Message>( placeholder, value )
		.id( super::SEARCH_FIELD_ID )
		.font_size( 14.0 )
		.borderless( true )
		// 60% of the stock field height; Length::widget keeps the same
		// widget-scaling behaviour as the default.
		.height( ltk::Length::widget( 29.0 ) )
		.on_change( on_search )
		.on_submit( Message::LauncherSearchSubmit )
		.into();

	let d = ltk::density().max( 1.0 );
	let mag: ltk::Element<Message> = match launcher.icons().search()
	{
		Some( ( raw, w, h ) ) =>
		{
			let tinted = launcher.icons().tint_cached( raw, palette.text_secondary );
			ltk::img_widget( tinted, *w, *h )
				.size( 18.0 * d, 18.0 * d )
				.into()
		}
		None => ltk::spacer().width( 18.0 * d ).into(),
	};

	let row = ltk::row::<Message>()
		.spacing( 8.0 * d )
		.padding( 0.0 )
		.push( ltk::flex( field ) )
		.push( mag );

	ltk::container::<Message>( row )
		.surface( "surface-card-flat" )
		.radius( 24.0 * d )
		.padding_h( 16.0 * d )
		.padding_v( 0.0 )
		.into()
}

// =============================================================================
// Left rail (desktop only)
// =============================================================================

/// Vertical column of monochrome session / launcher action glyphs.
/// All six icons cluster at the bottom of the rail in
/// [`AppLauncher::rail_actions`] order — Logout, Settings, Suspend,
/// Reboot, Power, AppToggle — with the launcher toggle flush against
/// the bottom edge. A flex spacer above absorbs whatever vertical
/// slack the rail has, so the cluster stays anchored regardless of
/// screen height.
fn rail_column( launcher: &AppLauncher, palette: &ltk::Palette ) -> ltk::Element<Message>
{
	let actions = AppLauncher::rail_actions();
	let d       = ltk::density().max( 1.0 );

	let mut icons = ltk::column::<Message>()
		.padding( 0.0 )
		.spacing( RAIL_GAP * d )
		.fit_content()
		.align_center_x( true );
	for ( icon_id, msg ) in actions.iter()
	{
		icons = icons.push( rail_icon( launcher.icons(), *icon_id, msg.clone(), palette ) );
	}

	let column = ltk::column::<Message>()
		.padding( 0.0 )
		.spacing( 0.0 )
		.fit_content()
		.align_center_x( true )
		.push( ltk::spacer() )
		.push( icons );

	// Outer container reserves the rail's pixel width and the
	// vertical breathing room the column itself can't paint (Column
	// only has a single `padding(p)` knob).
	let pad_h = ( ( RAIL_WIDTH - RAIL_ICON_PX ) * d / 2.0 ).max( 0.0 );
	ltk::container::<Message>( column )
		.padding_h( pad_h )
		.padding_v( PANEL_PAD * d )
		.into()
}

fn rail_icon(
	icons:   &Icons,
	which:   super::RailIcon,
	msg:     Message,
	palette: &ltk::Palette,
) -> ltk::Element<Message>
{
	let px = RAIL_ICON_PX * ltk::density().max( 1.0 );
	let icon: ltk::Element<Message> = match icons.rail( which )
	{
		Some( ( raw, w, h ) ) =>
		{
			let tinted = icons.tint_cached( raw, palette.text_primary );
			ltk::img_widget( tinted, *w, *h )
				.size( px, px )
				.into()
		}
		None => ltk::spacer().width( px ).into(),
	};

	ltk::pressable( icon ).on_press( msg ).into()
}

// =============================================================================
// Desktop sections (Pinned / Recently used / All Apps)
// =============================================================================

fn desktop_sections(
	launcher:      &AppLauncher,
	config:        &Config,
	visible_apps:  &[ AppEntry ],
	icon_cache:    &mut IconCache,
	screen_width:  u32,
	palette:       &ltk::Palette,
	on_launch:     &impl Fn( String ) -> Message,
	on_drag_start: &impl Fn( String ) -> Message,
) -> ltk::Element<Message>
{
	let cols = column_count( screen_width, true );

	// Pinned: dock items (the user's favourites bar). Reuses the
	// same on-disk source of truth so adding to / removing from the
	// dock immediately re-shapes this section.
	let pinned: Vec<AppEntry> = config.dock.items.iter()
		.filter_map( |item|
		{
			let crate::config::DockItem::App { app_id } = item;
			visible_apps.iter().find( |a| &a.app_id == app_id ).cloned()
		} )
		.collect();

	// Recently used: in-memory list ordered by most recent first.
	let recents: Vec<AppEntry> = launcher.recents_ids().iter()
		.filter_map( |id| visible_apps.iter().find( |a| &a.app_id == id ).cloned() )
		.collect();

	let d = ltk::density().max( 1.0 );
	let mut col = ltk::column::<Message>()
		.padding( 0.0 )
		.spacing( 16.0 * d );

	if !pinned.is_empty()
	{
		col = col
			.push( section_label( t!( "launcher_pinned" ).to_string(), palette ) )
			.push( apps_grid(
				launcher, &pinned, icon_cache, cols, ICON_PX * d, DESKTOP_SPACING_X * d, DESKTOP_SPACING_Y * d, DESKTOP_LABEL_PX, false, palette,
				on_launch, on_drag_start,
			) );
	}

	if !recents.is_empty()
	{
		col = col
			.push( section_label( t!( "launcher_recents" ).to_string(), palette ) )
			.push( apps_grid(
				launcher, &recents, icon_cache, cols, ICON_PX * d, DESKTOP_SPACING_X * d, DESKTOP_SPACING_Y * d, DESKTOP_LABEL_PX, false, palette,
				on_launch, on_drag_start,
			) );
	}

	col = col
		.push( section_label( t!( "launcher_all" ).to_string(), palette ) )
		.push( apps_grid(
			launcher, visible_apps, icon_cache, cols, ICON_PX * d, DESKTOP_SPACING_X * d, DESKTOP_SPACING_Y * d, DESKTOP_LABEL_PX, false, palette,
			on_launch, on_drag_start,
		) );

	col.into()
}

fn section_label( text: String, palette: &ltk::Palette ) -> ltk::Element<Message>
{
	ltk::text( text )
		.size( 14.0 )
		.color( palette.text_secondary )
		.into()
}

// =============================================================================
// Mobile / portrait — single flat grid (also used for filtered desktop)
// =============================================================================

fn flat_grid(
	launcher:      &AppLauncher,
	visible_apps:  &[ AppEntry ],
	icon_cache:    &mut IconCache,
	screen_width:  u32,
	is_landscape:  bool,
	palette:       &ltk::Palette,
	on_launch:     &impl Fn( String ) -> Message,
	on_drag_start: &impl Fn( String ) -> Message,
) -> ltk::Element<Message>
{
	if visible_apps.is_empty()
	{
		let msg = if !launcher.search_query.is_empty()
		{
			t!( "launcher_no_match", query = launcher.search_query.clone() ).to_string()
		}
		else
		{
			t!( "launcher_no_apps" ).to_string()
		};
		return ltk::column::<Message>()
			.padding( 0.0 )
			.spacing( 0.0 )
			.push( ltk::spacer().height( 32.0 * ltk::density().max( 1.0 ) ) )
			.push(
				ltk::text( msg )
					.size( 16.0 )
					.color( palette.text_secondary )
					.align_center(),
			)
			.into();
	}

	if is_landscape
	{
		let d    = ltk::density().max( 1.0 );
		let cols = column_count( screen_width, true );
		apps_grid( launcher, visible_apps, icon_cache, cols, ICON_PX * d, DESKTOP_SPACING_X * d, DESKTOP_SPACING_Y * d, DESKTOP_LABEL_PX, false, palette, on_launch, on_drag_start )
	}
	else
	{
		let w     = screen_width as f32;
		let icon  = ( w * MOBILE_ICON_FRAC ).round();
		let gap   = ( w * MOBILE_GAP_FRAC ).round();
		let label = ( w * MOBILE_LABEL_FRAC ).round();
		apps_grid( launcher, visible_apps, icon_cache, MOBILE_COLS as usize, icon, gap, gap, label, true, palette, on_launch, on_drag_start )
	}
}

// =============================================================================
// Shared grid + cell helpers
// =============================================================================

pub fn column_count( screen_width: u32, is_landscape: bool ) -> usize
{
	if is_landscape
	{
		// Cell size follows the output density so the count tracks the
		// logical width; the cap only guards absurd counts on ultra-wide
		// low-density outputs.
		let cell = CELL_PX * ltk::density().max( 1.0 );
		( ( screen_width as f32 / cell ).floor() as usize ).clamp( 6, 16 )
	}
	else
	{
		MOBILE_COLS as usize
	}
}

fn apps_grid(
	launcher:      &AppLauncher,
	apps:          &[ AppEntry ],
	icon_cache:    &mut IconCache,
	cols:          usize,
	icon_px:       f32,
	spacing_x:     f32,
	spacing_y:     f32,
	label_px:      f32,
	center:        bool,
	palette:       &ltk::Palette,
	on_launch:     &impl Fn( String ) -> Message,
	on_drag_start: &impl Fn( String ) -> Message,
) -> ltk::Element<Message>
{
	let mut grid = ltk::grid::<Message>( cols ).spacing_x( spacing_x ).spacing_y( spacing_y ).padding( 0.0 );
	let highlight = ( !launcher.search_query.is_empty() ).then_some( launcher.search_selected );
	for ( i, app ) in apps.iter().enumerate()
	{
		let cell = app_cell( launcher, app, icon_cache, icon_px, label_px, center, palette, on_launch, on_drag_start );
		let cell = if highlight == Some( i )
		{
			let d = ltk::density().max( 1.0 );
			ltk::container::<Message>( cell )
				.background( palette.surface_alt )
				.radius( 12.0 * d )
				.padding( 4.0 * d )
				.into()
		}
		else
		{
			cell
		};
		grid = grid.push( cell );
	}
	grid.into()
}

fn app_cell(
	launcher:      &AppLauncher,
	app:           &AppEntry,
	icon_cache:    &mut IconCache,
	icon_px:       f32,
	label_px:      f32,
	center:        bool,
	palette:       &ltk::Palette,
	on_launch:     &impl Fn( String ) -> Message,
	on_drag_start: &impl Fn( String ) -> Message,
) -> ltk::Element<Message>
{
	let drag_msg = on_drag_start( app.app_id.clone() );

	let ( rgba, w, h ) = match app.icon.as_ref()
		.and_then( |p| icon_cache.get_or_load_sized( p, icon_px as u32 ) )
	{
		Some( triple ) => triple,
		None           => launcher.default_icon().clone(),
	};

	let icon = ltk::icon_button::<Message>( rgba, w, h )
		.icon_size( icon_px )
		.variant( ltk::ButtonVariant::Secondary )
		.on_press( on_launch( app.app_id.clone() ) )
		.on_drag_start( drag_msg )
		.into_element();

	let col = ltk::column::<Message>().padding( 0.0 ).spacing( label_px * 0.5 );
	let col = if center { col.align_center_x( true ) } else { col };
	col
		.push( icon )
		.push(
			ltk::text( &app.name )
				.size( label_px )
				.color( palette.text_secondary )
				.align_center(),
		)
		.into()
}
