use std::sync::Arc;

use crate::config::DockItem;
use crate::launcher::AppEntry;
use crate::launcher::icon_cache::IconCache;

/// Inset of the dock pill chrome on the top + horizontal sides,
/// expressed as a fraction of the icon glyph height. Resolves
/// against `icon_size` at build time so the dock automatically
/// scales with the display: a 720 dp phone renders smaller icons
/// (cell × `ICON_FRACTION`) and gets proportionally tighter chrome,
/// a 1440 dp tablet gets roomier chrome under the same ratio.
/// 0.25 ≈ a quarter of an icon of breathing room — chunky enough
/// to read against the wallpaper's gradient through the glass.
const DOCK_PAD_FRACTION: f32 = 0.25;

/// Inset of the dock pill chrome on the bottom edge as a fraction
/// of the icon glyph height. The band below each icon cell hosts a
/// running-app indicator dot inside the cell itself
/// ([`DOT_GAP_FRACTION`] gap + [`DOT_DIAMETER_FRACTION`] dot); this
/// padding is only the trailing margin from the dot down to the
/// pill's bottom edge. Combined with the in-cell strip the visible
/// "below the icon" band is still ~22 px at a 48 px icon, matching
/// the original Figma "Dock Background" reservation, just split
/// between the cell column and the chrome.
const DOCK_BOTTOM_PAD_FRACTION: f32 = 0.225;

/// Vertical gap (as a fraction of icon size) between the icon
/// glyph's bottom edge and the top of the running-app indicator
/// dot. At a 48 px icon this lands at 6 px.
const DOT_GAP_FRACTION: f32 = 0.125;

/// Diameter of the running-app indicator dot, as a fraction of the
/// icon size. At a 48 px icon this lands at ≈5 px — matches the
/// design tokens for the "dot below" indicator.
const DOT_DIAMETER_FRACTION: f32 = 0.10;

/// Outer corner radius of the dock pill in logical px. A constant
/// (not a fraction of icon size) because the curve reads as a
/// design token in its own right — the Figma "Dock Background"
/// spec calls out 16 px specifically, and the rounded-top /
/// square-bottom silhouette breaks visually if the curvature
/// drifts with the icon footprint.
const DOCK_RADIUS_PX: f32 = 16.0;

/// Resolve a pinned `app_id` to the matching installed app. Exact match on
/// the `.desktop` filename (or future `.apk` id) — the launcher populates
/// `AppEntry::app_id` from the on-disk filename, so equality is stable
/// across renames of the displayed `Name`.
fn find_by_app_id<'a>( app_id: &str, apps: &'a [AppEntry] ) -> Option<&'a AppEntry>
{
	apps.iter().find( |a| a.app_id == app_id )
}

/// Total height of one dock cell column (icon glyph + dot-gap +
/// dot-strip). Used by the preview-slot spacer and any other code
/// that needs to match the live cell footprint without rebuilding
/// it.
fn dock_cell_height( icon_size: u32 ) -> f32
{
	( icon_size as f32 ) * ( 1.0 + DOT_GAP_FRACTION + DOT_DIAMETER_FRACTION )
}

/// Full vertical extent of the desktop dock pill chrome: top padding +
/// cell column (icon + dot strip) + bottom padding. Used by the
/// dock-overlay surface to know how tall to size itself.
pub fn desktop_pill_height( icon_size: u32 ) -> f32
{
	let pad_main   = ( icon_size as f32 ) * DOCK_PAD_FRACTION;
	let pad_bottom = ( icon_size as f32 ) * DOCK_BOTTOM_PAD_FRACTION;
	pad_main + dock_cell_height( icon_size ) + pad_bottom
}

/// Rendered width of the desktop dock pill in physical pixels. Kept in
/// lockstep with what [`build_dock`] lays out for `wrap_container = true`:
/// the launcher cell, one cell per *installed* pinned favourite, an
/// optional pinned↔running divider, one cell per running-but-unpinned
/// app, `spacing` between every row child, and the pill's horizontal
/// padding on both sides. The dock overlay derives its input region from
/// this so clicks beside the pill fall through to the window underneath
/// instead of being swallowed by the full-width dock surface.
pub fn desktop_pill_width(
	dock_items: &[DockItem],
	all_apps:   &[AppEntry],
	running:    &crate::running::RunningApps,
	icon_size:  u32,
	spacing:    f32,
) -> f32
{
	let icon = icon_size as f32;

	let pinned_cells = dock_items.iter()
		.filter( |item| { let DockItem::App { app_id } = item; find_by_app_id( app_id, all_apps ).is_some() } )
		.count();

	let pinned_ids: std::collections::HashSet<&str> = dock_items.iter()
		.map( |item| { let DockItem::App { app_id } = item; app_id.as_str() } )
		.collect();
	let temps = running.in_order().iter()
		.filter( |id| !pinned_ids.contains( id.as_str() ) )
		.count();

	let cells      = 1 + pinned_cells + temps;
	let has_sep    = temps > 0;
	let n_children = cells + has_sep as usize;
	let sep_w      = if has_sep { icon * 0.5 + 1.0 } else { 0.0 };
	let pad_main   = icon * DOCK_PAD_FRACTION;

	let row_w = cells as f32 * icon + sep_w
		+ ( n_children.saturating_sub( 1 ) as f32 ) * spacing;
	row_w + 2.0 * pad_main
}

/// Render the indicator dot below an icon — a coloured circle of
/// diameter `icon_size * DOT_DIAMETER_FRACTION` when `running`, the
/// same-sized invisible spacer otherwise so every cell keeps the
/// same vertical footprint regardless of running state and the
/// pill height does not jitter as apps come and go.
///
/// `palette.text_primary` is the dot colour — black in light mode,
/// white in dark mode, both come from the active theme document
/// so the dot tracks the rest of the chrome without an explicit
/// branch here.
fn dot_widget<Message: Clone + 'static>(
	icon_size: u32,
	palette:   &ltk::Palette,
	running:   bool,
) -> ltk::Element<Message>
{
	let dot_d = ( icon_size as f32 ) * DOT_DIAMETER_FRACTION;
	if running
	{
		ltk::container::<Message>( ltk::spacer().width( dot_d ).height( dot_d ) )
			.background( palette.text_primary )
			.radius( dot_d / 2.0 )
			.into()
	}
	else
	{
		ltk::spacer().width( dot_d ).height( dot_d ).into()
	}
}

/// Wrap an icon button in a `column { button, gap, dot-or-placeholder }`
/// so every dock cell has the same vertical footprint and the
/// running-app dot lands in the band the chrome reserves below the
/// icon. Used by both [`build_dock`] (pinned + temp items) and
/// [`build_dock_aligned`] (mobile grid cells).
fn build_cell<Message: Clone + 'static>(
	button:    ltk::Element<Message>,
	icon_size: u32,
	palette:   &ltk::Palette,
	running:   bool,
) -> ltk::Element<Message>
{
	let gap_h = ( icon_size as f32 ) * DOT_GAP_FRACTION;
	// `fit_content` is non-negotiable here: without it the cell column
	// reports `max_width` to its parent row, so each cell claims the
	// full row width, the icons end up spread across whatever the dock
	// is laid into, and the pill stretches to the screen edges. With
	// it the column's preferred width collapses to its widest child
	// (the icon button at `icon_size`), so the row stays content-sized
	// and the pill hugs its icons.
	ltk::column::<Message>()
		.padding( 0.0 )
		.spacing( 0.0 )
		.align_center_x( true )
		.fit_content()
		.push( button )
		.push( ltk::spacer().height( gap_h ) )
		.push( dot_widget( icon_size, palette, running ) )
		.into()
}

/// Vertical divider between the pinned items and the temporary
/// (running-but-not-pinned) items in the desktop dock. The line
/// itself spans the icon glyph's height — visible separation
/// without intruding into the dot band below — and is flanked by
/// quarter-icon gaps so the divider does not crowd the icons on
/// either side.
fn build_separator<Message: Clone + 'static>(
	icon_size: u32,
	palette:   &ltk::Palette,
) -> ltk::Element<Message>
{
	let gap_w  = ( icon_size as f32 ) * 0.25;
	let line_w = 1.0;
	let line: ltk::Element<Message> = ltk::container::<Message>(
		ltk::spacer().width( line_w ).height( icon_size as f32 ),
	)
		.background( palette.text_secondary )
		.into();

	ltk::row::<Message>()
		.padding( 0.0 )
		.spacing( 0.0 )
		.push( ltk::spacer().width( gap_w ) )
		.push( line )
		.push( ltk::spacer().width( gap_w ) )
		.into()
}

/// Build the dock for mobile or desktop mode.
/// `icon_size` and `spacing` control the visual density; when `wrap_container`
/// is true (desktop) the row is enclosed in a rounded semi-transparent pill.
///
/// `on_long_press` is invoked with `(dock_index, app_id)` to open the
/// edit-mode context menu (touch hold or right-click); `on_drag_start`
/// with the same payload to arm the drag (touch hold or mouse left-
/// button drag). Touch hold fires both; right-click fires only the
/// menu; mouse left-drag fires only the drag. The launcher button
/// (leftmost) is excluded from both flows — it is not a pinnable app,
/// it is a UI action.
///
/// `preview_at = Some(k)` opens an `icon_size`-wide empty slot
/// immediately before `dock_items[k]` (or at the end when
/// `k == dock_items.len()`). The shell sets it from the live drag
/// cursor so the user sees the dock part to make room *before* they
/// release. `None` renders the dock at rest.
pub fn build_dock<Message: Clone + 'static>(
	dock_items: &[DockItem],
	all_apps: &[AppEntry],
	icon_cache: &mut IconCache,
	logo: &( Arc<Vec<u8>>, u32, u32 ),
	default_icon: &( Arc<Vec<u8>>, u32, u32 ),
	launcher_msg: Message,
	on_launch: &impl Fn( String ) -> Message,
	on_activate: &impl Fn( String ) -> Message,
	on_long_press: impl Fn( usize, String ) -> Message,
	on_long_press_temp: impl Fn( String ) -> Message,
	on_drag_start: impl Fn( usize, String ) -> Message,
	icon_size: u32,
	spacing: f32,
	wrap_container: bool,
	preview_at: Option<usize>,
	running: &crate::running::RunningApps,
) -> ltk::Element<Message>
{
	let palette = ltk::theme_palette();
	let cell_h  = dock_cell_height( icon_size );

	let mut row = ltk::row::<Message>()
		.padding( 0.0 )
		.spacing( spacing );

	// Match the live cell height (icon + dot strip) so the row stays
	// flush when the drop preview opens a hole — a slot the height of
	// just the icon would tear the bottom edge of the pill.
	let empty_slot = | r: ltk::Row<Message> | -> ltk::Row<Message>
	{
		r.push( ltk::spacer().width( icon_size as f32 ).height( cell_h ) )
	};

	// Build one pinned / temporary icon button. The drag / menu wiring
	// is optional because temporary (running-but-not-pinned) items
	// today are launch-only — there is no meaningful "swap dock
	// position" semantic for a slot the user did not pin.
	//
	// Dock buttons opt out of keyboard focus (`.focusable( false )`):
	// the homescreen is a touch surface, there is no Tab target here
	// and a persistent focus ring after every tap looks broken. They
	// remain hit-testable thanks to `Element::is_interactive()`
	// overriding to `true` for `Button`.
	let make_btn = |
		app:       &AppEntry,
		on_press:  Message,
		lp_msg:    Option<Message>,
		ds_msg:    Option<Message>,
		ic:        &mut IconCache,
	| -> ltk::Element<Message>
	{
		// Three rendering paths because the icon-button widget paints
		// raster icons and the plain `button` widget paints text; the
		// fallback drops to text when the `.desktop` either declares
		// no icon or the catalogue lookup misses on disk.
		if let Some( ref icon_path ) = app.icon
		{
			if let Some( ( arc_rgba, w, h ) ) = ic.get_or_load_sized( icon_path, icon_size )
			{
				let mut b = ltk::icon_button::<Message>( arc_rgba, w, h )
					.icon_size( icon_size as f32 )
					.variant( ltk::ButtonVariant::Secondary )
					.focusable( false )
					.tooltip( app.name.clone() )
					.on_press( on_press );
				if let Some( msg ) = lp_msg { b = b.on_long_press( msg ); }
				if let Some( msg ) = ds_msg { b = b.on_drag_start( msg ); }
				return b.into_element();
			}
		}
		let mut b = ltk::icon_button::<Message>(
			Arc::clone( &default_icon.0 ), default_icon.1, default_icon.2,
		)
			.icon_size( icon_size as f32 )
			.variant( ltk::ButtonVariant::Secondary )
			.focusable( false )
			.tooltip( app.name.clone() )
			.on_press( on_press );
		if let Some( msg ) = lp_msg { b = b.on_long_press( msg ); }
		if let Some( msg ) = ds_msg { b = b.on_drag_start( msg ); }
		b.into_element()
	};

	// Launcher button — always leftmost. No running-app dot (it is a
	// UI action, not an app), but the cell still goes through
	// `build_cell` with `running = false` so its column height matches
	// the app cells and the row stays flush.
	let launcher_btn = ltk::icon_button::<Message>( Arc::clone( &logo.0 ), logo.1, logo.2 )
		.icon_size( icon_size as f32 )
		.variant( ltk::ButtonVariant::Secondary )
		.focusable( false )
		.on_press( launcher_msg.clone() )
		.into_element();
	row = row.push( build_cell( launcher_btn, icon_size, &palette, false ) );

	for ( index, item ) in dock_items.iter().enumerate()
	{
		if preview_at == Some( index )
		{
			row = empty_slot( row );
		}
		let DockItem::App { app_id } = item;
		let Some( app ) = find_by_app_id( app_id, all_apps ) else
		{
			// Configured favorite is not installed on this system — skip it
			// silently rather than render a placeholder. Same logic applies to
			// APK entries when the translation layer is absent.
			continue;
		};
		let is_running = running.is_running( app_id );
		let lp_msg     = on_long_press( index, app_id.clone() );
		let ds_msg     = on_drag_start( index, app_id.clone() );
		let press_msg  = if is_running
		{
			let wl = running.wayland_id_for( app_id ).map( str::to_string ).unwrap_or_else( || app_id.clone() );
			on_activate( wl )
		} else {
			on_launch( app_id.clone() )
		};
		let btn        = make_btn( app, press_msg, Some( lp_msg ), Some( ds_msg ), icon_cache );
		row = row.push( build_cell( btn, icon_size, &palette, is_running ) );
	}
	// `preview_at == Some(dock_items.len())` ⇒ append at end.
	if preview_at == Some( dock_items.len() )
	{
		row = empty_slot( row );
	}

	// Desktop dock only: append "temporary" cells for any app
	// currently running that has no pinned slot. A gap-line-gap
	// divider separates pinned from temp icons; pinned-vs-temp is
	// determined by exact `app_id` equality against the dock's
	// configured items (the running-state map keys on the same
	// `.desktop` filename `dock_items` uses, the matcher in
	// `crate::running::match_app_id` already normalises Wayland
	// `app_id`s to that form).
	if wrap_container
	{
		let pinned: std::collections::HashSet<&str> = dock_items.iter()
			.map( |item| { let DockItem::App { app_id } = item; app_id.as_str() } )
			.collect();
		let temps: Vec<&str> = running.in_order().iter()
			.filter( |id| !pinned.contains( id.as_str() ) )
			.map( |s| s.as_str() )
			.collect();
		if !temps.is_empty()
		{
			row = row.push( build_separator( icon_size, &palette ) );
			for app_id in &temps
			{
				let wl     = running.wayland_id_for( app_id ).map( str::to_string ).unwrap_or_else( || app_id.to_string() );
				let press  = on_activate( wl );
				let lp_msg = on_long_press_temp( app_id.to_string() );
				let btn = match find_by_app_id( app_id, all_apps )
				{
					Some( app ) => make_btn( app, press, Some( lp_msg ), None, icon_cache ),
					None        =>
					{
						ltk::icon_button::<Message>( Arc::clone( &default_icon.0 ), default_icon.1, default_icon.2 )
							.icon_size( icon_size as f32 )
							.variant( ltk::ButtonVariant::Secondary )
							.focusable( false )
							.tooltip( app_id.to_string() )
							.on_press( press )
							.on_long_press( lp_msg )
							.into_element()
					}
				};
				row = row.push( build_cell( btn, icon_size, &palette, true ) );
			}
		}
	}

	if wrap_container
	{
		// Glass dock chrome — cyan→navy (dark) / white→off-white (light)
		// gradient with backdrop blur 22.5 and the panel-family inner-
		// shadow stack. Mirrors the Figma "Dock Background" spec; the
		// surface-card slot already carries that exact recipe in the
		// default ltk theme so we just plug in.
		//
		// `Corners::top( DOCK_RADIUS_PX )` matches the Figma's
		// `border-radius: 16 16 0 0`: rounded top corners, square
		// bottom corners. The dock pill sits flush against the
		// screen bottom in landscape, so the bottom edge reads as a
		// clean horizontal line against the screen edge while the
		// top arches over the wallpaper.
		//
		// Padding is asymmetric and derived from the live icon size
		// so the chrome scales with the display (the icon size is
		// `cell * ICON_FRACTION` from `homescreen::grid_dims`, which
		// is already screen-resolution-driven). The bottom padding
		// is now just the trailing margin from the dot to the pill
		// edge — the dot-band (gap + dot) lives inside each cell
		// column, so the visible "below the icon" reservation is
		// split between `DOCK_BOTTOM_PAD_FRACTION` and the cell
		// height delta from [`dock_cell_height`].
		let pad_main   = ( icon_size as f32 ) * DOCK_PAD_FRACTION;
		let pad_bottom = ( icon_size as f32 ) * DOCK_BOTTOM_PAD_FRACTION;
		ltk::container( row )
			.surface( "surface-card" )
			.padding( pad_main )
			.padding_bottom( pad_bottom )
			.radius( ltk::Corners::top( DOCK_RADIUS_PX * ltk::density().max( 1.0 ) ) )
			.into()
	} else {
		row.into()
	}
}

/// Build the mobile dock as a column-aligned grid that mirrors the
/// homescreen icon grid above it.
///
/// Each dock slot is a `cell_w`-wide cell with the launcher button or a
/// dock app icon centred in it. With the row wrapped in
/// `padding_h(side_pad)` and using the same `cell_w` / `spacing_x` /
/// column count as the icon grid, every dock icon's horizontal centre
/// lines up with the corresponding column of icons above. The launcher
/// pinned at index 0 uses the first cell; the user's dock favourites
/// fill cells 1..; trailing cells (if the user has fewer favourites
/// than `cols - 1`) stay visually empty so the grid alignment is
/// preserved.
///
/// `icon_size` is the icon glyph footprint (matches the grid icon
/// derived from `cell * ICON_FRACTION` in the homescreen module). This
/// is the path the mobile / portrait homescreen takes; desktop /
/// landscape continues through [`build_dock`] with its compact
/// surface-pill row.
pub fn build_dock_aligned<Message: Clone + 'static>(
	dock_items: &[DockItem],
	all_apps: &[AppEntry],
	icon_cache: &mut IconCache,
	on_launch: &impl Fn( String ) -> Message,
	on_activate: &impl Fn( String ) -> Message,
	on_long_press: impl Fn( usize, String ) -> Message,
	on_drag_start: impl Fn( usize, String ) -> Message,
	icon_size: u32,
	cols: usize,
	cell_w: f32,
	spacing_x: f32,
	side_pad: f32,
	running: &crate::running::RunningApps,
) -> ltk::Element<Message>
{
	let palette = ltk::theme_palette();
	let gap_h   = ( icon_size as f32 ) * DOT_GAP_FRACTION;

	// Each cell is a fixed-width column with the icon centred, plus a
	// gap and the running-app indicator dot below. The leading
	// zero-height spacer sets the column's allocated width to the
	// homescreen grid's `cell_w` so dock icons line up with the
	// columns of the icon grid above. Same pattern as the homescreen
	// grid cells use for their icon + label stack.
	let cell = | btn: ltk::Element<Message>, is_running: bool | -> ltk::Element<Message>
	{
		ltk::column::<Message>()
			.padding( 0.0 )
			.spacing( 0.0 )
			.align_center_x( true )
			.push( ltk::spacer().width( cell_w ).height( 0.0 ) )
			.push( btn )
			.push( ltk::spacer().height( gap_h ) )
			.push( dot_widget( icon_size, &palette, is_running ) )
			.into()
	};

	let mut grid = ltk::grid::<Message>( cols )
		.spacing_x( spacing_x )
		.spacing_y( 0.0 )
		.padding( 0.0 );

	// Mobile dock is all favourites (no launcher cell): every one of
	// the `cols` columns maps 1:1 to a grid column above.
	let max_dock_slots = cols;
	for ( index, item ) in dock_items.iter().take( max_dock_slots ).enumerate()
	{
		let DockItem::App { app_id } = item;
		let Some( app ) = find_by_app_id( app_id, all_apps ) else
		{
			// Pinned favourite is missing on this host. Push an empty
			// cell instead of skipping so the slot reservation stays
			// aligned with the column above (skipping would shift every
			// later favourite one cell to the left).
			grid = grid.push( cell( ltk::spacer().width( cell_w ).height( icon_size as f32 ).into(), false ) );
			continue;
		};
		let is_running = running.is_running( app_id );
		let lp_msg     = on_long_press( index, app_id.clone() );
		let ds_msg     = on_drag_start( index, app_id.clone() );
		let press_msg  = if is_running
		{
			let wl = running.wayland_id_for( app_id ).map( str::to_string ).unwrap_or_else( || app_id.clone() );
			on_activate( wl )
		} else {
			on_launch( app_id.clone() )
		};
		let btn: ltk::Element<Message> = if let Some( ref icon_path ) = app.icon
		{
			if let Some( ( arc_rgba, w, h ) ) = icon_cache.get_or_load_sized( icon_path, icon_size )
			{
				ltk::icon_button::<Message>( arc_rgba, w, h )
					.icon_size( icon_size as f32 )
					.variant( ltk::ButtonVariant::Secondary )
					.focusable( false )
					.on_press( press_msg )
					.on_long_press( lp_msg )
					.on_drag_start( ds_msg )
					.into_element()
			} else {
				ltk::button::<Message>( &app.name )
					.variant( ltk::ButtonVariant::Secondary )
					.focusable( false )
					.on_press( press_msg )
					.on_long_press( lp_msg )
					.on_drag_start( ds_msg )
					.into_element()
			}
		} else {
			ltk::button::<Message>( &app.name )
				.variant( ltk::ButtonVariant::Secondary )
				.focusable( false )
				.on_press( press_msg )
				.on_long_press( lp_msg )
				.on_drag_start( ds_msg )
				.into_element()
		};
		grid = grid.push( cell( btn, is_running ) );
	}

	// Pad trailing empty slots so the grid always reports `cols` cells
	// wide — needed by `WrapGrid` to keep the dock row width identical
	// to the icon grid above it (and therefore the column centres).
	let used = dock_items.len().min( max_dock_slots );
	for _ in used..cols
	{
		grid = grid.push( cell( ltk::spacer().width( cell_w ).height( icon_size as f32 ).into(), false ) );
	}

	ltk::container( grid )
		.padding_h( side_pad )
		.into()
}
