//! Screen-relative sizing primitives + homescreen grid metrics.
//!
//! Single source of truth for everything that turns a "size that read
//! right on a 480 dp phone" into a real pixel value on the active
//! surface. The renderer ([`super::view`]) and the drop hit-tester
//! ([`super::drop`]) both consume these values, so there's no risk of
//! one painting a 7×4 grid while the other thinks it's 6×4. Constants
//! live alongside the helpers that consume them — pull this file into
//! a new editor tab to tweak the layout numbers without leaving them
//! islanded across the rest of the homescreen module.

// =============================================================================
// Screen-relative sizing
// =============================================================================

/// Reference dimension the layout was tuned against. Every absolute
/// pixel value below is "the size that read right on a 480 dp surface";
/// real surfaces scale linearly via [`dp`]. Picking a single anchor
/// keeps the values reviewable as plain numbers (88 dp cell, 16 dp
/// margin, …) without sacrificing proportional rendering on tablets,
/// desktop or smaller phones.
const SCREEN_UNIT_REF: f32 = 480.0;

/// Reference unit for screen-relative sizing — the smaller of width
/// and height. On a phone in portrait this is `width`; in landscape it
/// flips to `height`. Returning the smaller dimension keeps chrome
/// visually proportional across rotations: a `dp(16)` margin reads as
/// the same fraction of the visible chrome regardless of orientation.
fn screen_unit( screen_w: u32, screen_h: u32 ) -> f32
{
	( screen_w.min( screen_h ) as f32 ).max( 1.0 )
}

/// Convert a value tuned for the [`SCREEN_UNIT_REF`] phone into pixels
/// for the actual surface.
///
/// Two scaling regimes — orientation drives the choice because the
/// layouts they back are designed against different conventions:
///
/// - **Portrait** (`width ≤ height`, the mobile-paged surface): linear
///   in `min(w, h) / SCREEN_UNIT_REF`. `dp(88)` is 88 px on a 480 dp
///   phone, 66 px on a 360 dp phone, 132 px on a 720 dp phablet. The
///   user-facing intent is "icons stay the same fraction of the
///   screen" so the chrome breathes consistently across phone sizes.
///
/// - **Landscape** (`width > height`, the desktop / single-page
///   surface): logical px times the output density. `dp(88)` is 88
///   physical px at density 1 on every desktop resolution — the same
///   way macOS / GNOME / Windows size their icons in absolute logical
///   pixels regardless of monitor dimensions — and doubles on a
///   scale-2 output. The alternative — scaling `dp` by `min(w,h)` like
///   portrait does — doubled icons on a 1600 × 900 surface (`min =
///   900`, factor 1.875) and made the homescreen feel cramped, because
///   the constraining dimension on a desktop is no longer the screen
///   size but the user's expectation of a fixed-logical-pixel tile.
pub( super ) fn dp( value: f32, screen_w: u32, screen_h: u32 ) -> f32
{
	let is_landscape = screen_w > screen_h;
	if is_landscape
	{
		value * ltk::density().max( 1.0 )
	}
	else
	{
		value * screen_unit( screen_w, screen_h ) / SCREEN_UNIT_REF
	}
}

// =============================================================================
// Grid sizing constants
// =============================================================================

/// Target cell edge expressed against the [`SCREEN_UNIT_REF`] phone. A
/// cell fits a 48-ish dp icon, a two-line label, and generous
/// breathing room around both; sits between an iOS Springboard tile
/// and a GNOME grid cell. Actual cell size flexes per surface (via
/// [`dp`]) and is clamped to `[CELL_MIN_DP, CELL_MAX_DP]`.
const CELL_TARGET_DP: f32 = 88.0;
const CELL_MIN_DP:    f32 = 80.0;
const CELL_MAX_DP:    f32 = 112.0;

/// Horizontal inset on the grid in landscape (in dp). Portrait derives
/// its inset from [`MOBILE_GAP_FRAC`] via [`grid_side_pad`] so the edge
/// inset equals the inter-column gap.
pub( super ) const SIDE_PAD_LANDSCAPE_DP: f32 = 24.0;

/// Grid spacing between cells (in dp). Landscape only — portrait
/// derives its spacing from [`MOBILE_GAP_FRAC`].
const GRID_SPACING_DP: f32 = 4.0;

/// Horizontal inset from the screen edge to the first / last grid
/// column. Landscape uses a fixed dp value; portrait uses the
/// inter-column gap so the edge inset and the gaps are identical and
/// the five columns fill the width exactly.
pub( crate ) fn grid_side_pad( screen_w: u32, screen_h: u32, is_landscape: bool ) -> f32
{
	if is_landscape
	{
		dp( SIDE_PAD_LANDSCAPE_DP, screen_w, screen_h )
	}
	else
	{
		( screen_w as f32 * MOBILE_GAP_FRAC ).round()
	}
}

/// Forge's topbar height in physical px — overlays the homescreen
/// surface from `y = 0`. Forge sizes its topbar in logical px times the
/// output scale, so the reserve follows the density, not [`dp`].
fn topbar_px() -> f32
{
	crate::config::TOPBAR_PX as f32 * ltk::density().max( 1.0 )
}

/// Half-height of the dock's bottom inset (in dp). The inset itself —
/// `dock_edge_margin * 2` — is what `dock_band` and the view's bottom
/// spacer apply, the doubling stays at the call site. Mobile portrait
/// gets a slightly larger value so the dock breathes against the
/// rounded phone bezel; landscape's denser chrome can sit tighter.
const DOCK_EDGE_HALF_PORTRAIT_DP:  f32 = 16.0;
const DOCK_EDGE_HALF_LANDSCAPE_DP: f32 = 12.0;

/// Single-side dock-edge inset for the active orientation, scaled to
/// the active surface. The "half" name reflects that the visible inset
/// from the dock to the screen edge is `2 *` this value (the bottom
/// spacer in `view` and the offset in `dock_band` both double it).
pub( crate ) fn dock_edge_half( screen_w: u32, screen_h: u32, is_landscape: bool ) -> f32
{
	let v = if is_landscape { DOCK_EDGE_HALF_LANDSCAPE_DP } else { DOCK_EDGE_HALF_PORTRAIT_DP };
	dp( v, screen_w, screen_h )
}

/// Vertical reserve at the top of every page so (a) the icon grid
/// clears forge's topbar and (b) the visible inset from the topbar's
/// lower edge matches the dock's inset from the screen's bottom edge.
/// The user-facing intent is symmetry — the grid should read as
/// centred between the two chrome bars instead of crowded against the
/// topbar. Returns `TOPBAR_PX + dock_edge_margin`, with
/// `dock_edge_margin = 2 * dock_edge_half` matched to the value used
/// in [`super::view`] / [`dock_band`] so the symmetry holds in both
/// orientations.
pub( super ) fn grid_top_reserve( screen_w: u32, screen_h: u32, is_landscape: bool ) -> f32
{
	topbar_px() + dock_edge_half( screen_w, screen_h, is_landscape ) * 2.0
}

/// Fraction of the cell edge taken up by the icon glyph in the
/// homescreen grid. Lifted from a previous 0.60 — the launcher
/// Springboard look the homescreen targets reads with chunkier
/// icons; 0.72 leaves enough room below for the label without
/// crowding sibling cells. Mobile dock icons inherit this fraction
/// so a dock favourite sits in a glyph footprint identical to the
/// column above it.
pub( super ) const ICON_FRACTION: f32 = 0.72;

/// Desktop grid label size, in **logical** pixels: ltk resolves font
/// sizes against the logical viewport and multiplies by `dpi_scale` at
/// raster time, so this must not be pre-multiplied by the density.
pub( super ) const LABEL_PX_LANDSCAPE: f32 = 10.0;

/// Icon-to-label gap as a fraction of the label size. Column spacing is
/// geometry, which resolves in **physical** pixels, so callers apply the
/// density the label itself only picks up when it is rasterised.
pub( super ) const LABEL_GAP_FRACTION: f32 = 0.8;

/// Mobile (portrait) app-grid sizing as fractions of the physical
/// screen width. Single source of truth shared by the homescreen grid,
/// the dock and the app launcher so all three line up: [`MOBILE_COLS`]
/// columns of [`MOBILE_ICON_FRAC`] icons with an equal [`MOBILE_GAP_FRAC`]
/// gap between every column and at both edges. The identity
/// `COLS·ICON + (COLS+1)·GAP = 1` makes the row fill the width exactly
/// at any resolution (`5·0.13 + 6·0.0583 ≈ 1`). The label is a separate
/// fraction so it does not grow with the icon.
pub( crate ) const MOBILE_COLS:       u32 = 5;
pub( crate ) const MOBILE_ICON_FRAC:  f32 = 0.13;
pub( crate ) const MOBILE_GAP_FRAC:   f32 = 0.0583;
pub( crate ) const MOBILE_LABEL_FRAC: f32 = 0.0187;

/// Portrait counterpart of [`LABEL_GAP_FRACTION`], kept at its tuned
/// value: the mobile label is a fraction of the physical width, not a
/// logical font size, so it does not want the density correction.
pub( super ) const MOBILE_LABEL_GAP_FRACTION: f32 = 0.5;

/// Fraction of the cell edge for the **desktop** dock pill icons.
/// Smaller than [`ICON_FRACTION`] (the homescreen-grid number)
/// because the dock chrome boxes the icons in a tighter glass
/// envelope: matching grid sizes reads as overcrowded once the
/// `surface-card` background, the asymmetric padding, and the
/// running-app indicator dots come into the same pill. 0.60 vs
/// 0.72 puts dock icons at ~83 % of the grid icon size — clearly
/// smaller without losing legibility against the wallpaper.
const DOCK_ICON_FRACTION: f32 = 0.60;

// =============================================================================
// Page indicator metrics
// =============================================================================

/// Page indicator strip metrics, expressed against [`SCREEN_UNIT_REF`]
/// and scaled to the actual surface via [`dp`] / [`page_indicator`].
///
/// At 4×4 dots the indicator read as a render glitch on a 480 dp
/// phone and gave the user nothing to aim at; at 8×8 dots and 32×8
/// dashes the dot–dash pattern is recognisable as a paging widget
/// without dominating the chrome. On a wider surface the same
/// proportions scale up via `dp`.
const PAGE_IND_HEIGHT_DP: f32 = 8.0;
const PAGE_IND_DASH_W_DP: f32 = 32.0;
const PAGE_IND_DOT_W_DP:  f32 = 8.0;
/// Horizontal gap between adjacent pressable cells (dp).
const PAGE_IND_GAP_DP:    f32 = 12.0;
/// Vertical padding inside each pill's pressable wrapper (dp). Inflates
/// the tap target without inflating the visible pill — horizontal
/// padding stays at zero, the row spacing already provides the visible
/// gap.
const PAGE_IND_TAP_V_DP:  f32 = 12.0;
/// Vertical gap above and below the indicator strip (dp).
const PAGE_IND_MARGIN_DP: f32 = 16.0;

/// Concrete pixel metrics for the page indicator on a given surface.
/// One struct, computed once by `view`, threaded to the renderer and
/// to `grid_dims` so the band reservation matches what
/// `build_page_indicator` actually paints.
#[ derive( Clone, Copy ) ]
pub( super ) struct PageIndicator
{
	pub height: f32,
	pub dash_w: f32,
	pub dot_w:  f32,
	pub gap:    f32,
	pub tap_v:  f32,
	pub margin: f32,
	/// Total vertical reserve in `dock_strip` (margin + tap-padded
	/// pill row + margin). `grid_dims` subtracts this from `avail_h`
	/// so the grid's last row clears the indicator.
	pub band:   f32,
}

pub( super ) fn page_indicator( screen_w: u32, screen_h: u32 ) -> PageIndicator
{
	let s = | v: f32 | dp( v, screen_w, screen_h );
	let height = s( PAGE_IND_HEIGHT_DP );
	let dash_w = s( PAGE_IND_DASH_W_DP );
	let dot_w  = s( PAGE_IND_DOT_W_DP );
	let gap    = s( PAGE_IND_GAP_DP );
	let tap_v  = s( PAGE_IND_TAP_V_DP );
	let margin = s( PAGE_IND_MARGIN_DP );
	let band   = height + 2.0 * tap_v + 2.0 * margin;
	PageIndicator { height, dash_w, dot_w, gap, tap_v, margin, band }
}

// =============================================================================
// Dock band + grid layout
// =============================================================================

/// Dock band metrics in logical pixels: `(top_y, bottom_y)`. Shared between
/// [`super::drop::drop_target`] (to identify the dock strip) and [`grid_dims`]
/// (to keep the grid from extending under it). All values scale with the
/// screen unit so the dock breathes proportionally on bigger surfaces;
/// the per-orientation dp anchors mirror the values [`crate::dock::build_dock`]
/// and the bottom-container margin in [`super::view`] use, so the band stays
/// pixel-true to what the renderer actually paints.
pub( crate ) fn dock_band( screen_w: u32, screen_h: u32, is_landscape: bool ) -> ( f32, f32 )
{
	let bot_margin = dock_edge_half( screen_w, screen_h, is_landscape );
	let height = if is_landscape
	{
		// Mirror `dock_geom_desktop` + `dock::desktop_pill_height` so the
		// band matches the painted pill instead of a hand-kept estimate.
		let ( _, cell_w ) = landscape_cols_cell( screen_w, screen_h );
		let icon = ( cell_w * DOCK_ICON_FRACTION ).round().max( 40.0 * ltk::density().max( 1.0 ) );
		crate::dock::desktop_pill_height( icon as u32 )
	}
	else
	{
		// Mobile dock cell = icon (`MOBILE_ICON_FRAC` wide) plus the
		// running-app dot strip below it. The strip adds the
		// `DOT_GAP_FRACTION` + `DOT_DIAMETER_FRACTION` of
		// `dock::build_dock_aligned` (≈0.225 of the icon); the band
		// mirrors that so the grid stops flush above the painted dock.
		let icon = ( screen_w as f32 * MOBILE_ICON_FRAC ).round();
		icon * 1.225
	};
	let bottom_y   = screen_h as f32 - bot_margin * 2.0;
	let top_y      = bottom_y - height;
	( top_y, bottom_y )
}

/// Result of [`grid_dims`]. `cell_w` is the column width and `cell_h` the
/// per-cell height — equal in landscape (square cells), distinct in
/// portrait where the cell is an icon stacked over a label. `spacing_x`
/// is the horizontal gap that makes `cols * cell_w + (cols-1) * spacing_x`
/// fit the available width exactly (matches WrapGrid's formula);
/// `spacing_y` distributes leftover vertical space between rows so the
/// grid reaches the dock flush with no trailing empty strip.
#[ derive( Clone, Copy ) ]
pub( crate ) struct GridDims
{
	pub cols:      u32,
	pub rows:      u32,
	pub cell_w:    f32,
	pub cell_h:    f32,
	pub spacing_x: f32,
	pub spacing_y: f32,
}

/// Compute the homescreen grid layout for a given logical-px screen size.
/// Portrait and landscape follow different conventions, so each has its
/// own builder; both return rows / spacing tuned so the grid reaches the
/// dock with no trailing empty strip.
pub( crate ) fn grid_dims( screen_w: u32, screen_h: u32, is_landscape: bool ) -> GridDims
{
	if is_landscape
	{
		grid_dims_landscape( screen_w, screen_h )
	}
	else
	{
		grid_dims_portrait( screen_w, screen_h )
	}
}

/// Portrait grid: a fixed [`MOBILE_COLS`] columns of [`MOBILE_ICON_FRAC`]-
/// wide cells, the inter-column gap and the edge inset both
/// [`MOBILE_GAP_FRAC`] so the row fills the width exactly. The cell
/// height stacks the icon over a [`MOBILE_LABEL_FRAC`] label with room
/// for a short two-line name. Rows is the largest count that fits above
/// the dock; the leftover vertical slack becomes `spacing_y` so the rows
/// spread evenly over the full height.
fn grid_dims_portrait( screen_w: u32, screen_h: u32 ) -> GridDims
{
	let w        = screen_w as f32;
	let cols     = MOBILE_COLS;
	let gap      = ( w * MOBILE_GAP_FRAC ).round();
	let cell_w   = ( w * MOBILE_ICON_FRAC ).round();
	let label    = ( w * MOBILE_LABEL_FRAC ).round();
	// Icon (square, == cell_w) + a 2px gap + room for a two-line label.
	let cell_h   = ( cell_w + 2.0 + label * 2.0 ).round();

	let ( dock_top_y, _ ) = dock_band( screen_w, screen_h, false );
	let indicator_reserve = page_indicator( screen_w, screen_h ).band;
	let top_reserve       = grid_top_reserve( screen_w, screen_h, false );
	let avail_h  = ( dock_top_y - indicator_reserve - top_reserve ).max( cell_h );

	let pitch = cell_h + gap;
	let rows  = ( ( avail_h + gap ) / pitch ).floor().max( 1.0 ) as u32;
	let spacing_y = if rows > 1
	{
		( ( avail_h - rows as f32 * cell_h ) / ( rows as f32 - 1.0 ) ).max( gap )
	} else {
		gap
	};
	GridDims { cols, rows, cell_w, cell_h, spacing_x: gap, spacing_y }
}

/// Landscape grid: square cells whose count flexes to keep the actual
/// cell width inside the `[CELL_MIN_DP, CELL_MAX_DP]` band (scaled to the
/// surface), walking from the `round(avail / cell_target)` seed. Cell
/// width matches WrapGrid's `(avail - spacing*(cols-1)) / cols` so
/// [`super::drop::drop_target`] and the renderer agree on every pixel.
/// The leftover vertical slack becomes `spacing_y`. A minimum of 2×2
/// keeps the grid well-formed on degenerate surfaces.
/// Width-only half of the landscape grid fit: the column count and cell
/// edge for `screen_w`. Split out of [`grid_dims_landscape`] so
/// [`dock_band`] can derive the painted dock pill from the real cell
/// without recursing through the height math (which itself consults
/// `dock_band`).
fn landscape_cols_cell( screen_w: u32, screen_h: u32 ) -> ( u32, f32 )
{
	let side_pad     = dp( SIDE_PAD_LANDSCAPE_DP, screen_w, screen_h );
	let cell_min     = dp( CELL_MIN_DP,    screen_w, screen_h );
	let cell_max     = dp( CELL_MAX_DP,    screen_w, screen_h );
	let cell_target  = dp( CELL_TARGET_DP, screen_w, screen_h );
	let grid_spacing = dp( GRID_SPACING_DP, screen_w, screen_h );
	let avail_w      = ( screen_w as f32 - side_pad * 2.0 ).max( cell_min * 2.0 );

	let cell_for = | c: u32 | -> f32
	{
		( avail_w - grid_spacing * ( c as f32 - 1.0 ) ).max( 0.0 ) / c as f32
	};

	let mut cols = ( avail_w / cell_target ).round().max( 2.0 ) as u32;
	loop
	{
		let cell = cell_for( cols );
		if cell > cell_max { cols += 1; continue; }
		if cell < cell_min && cols > 2 { cols -= 1; continue; }
		break;
	}
	( cols, cell_for( cols ) )
}

fn grid_dims_landscape( screen_w: u32, screen_h: u32 ) -> GridDims
{
	let cell_min     = dp( CELL_MIN_DP,    screen_w, screen_h );
	let grid_spacing = dp( GRID_SPACING_DP, screen_w, screen_h );
	let ( dock_top_y, _ ) = dock_band( screen_w, screen_h, true );
	let avail_h  = ( dock_top_y - grid_top_reserve( screen_w, screen_h, true ) ).max( cell_min * 2.0 );

	let ( cols, cell ) = landscape_cols_cell( screen_w, screen_h );
	let pitch = cell + grid_spacing;
	let rows  = ( ( avail_h + grid_spacing ) / pitch ).floor().max( 1.0 ) as u32;
	let spacing_y = if rows > 1
	{
		( ( avail_h - rows as f32 * cell ) / ( rows as f32 - 1.0 ) ).max( grid_spacing )
	} else {
		grid_spacing
	};
	GridDims { cols, rows, cell_w: cell, cell_h: cell, spacing_x: grid_spacing, spacing_y }
}

// =============================================================================
// Desktop dock geometry
// =============================================================================

/// Bundle of geometry values shared between the desktop dock's
/// renderer ([`crate::dock::build_dock`]), the drop-preview helper
/// ([`super::drop::dock_preview_index`]) and the capacity guard
/// ([`super::drop::dock_capacity`]). One source of truth so the three
/// stay pixel-aligned; if the icon glyph footprint changes, the only
/// place that gets edited is here.
#[ derive( Clone, Copy ) ]
pub( crate ) struct DockGeomDesktop
{
	pub icon:     f32,
	pub spacing:  f32,
	pub pill_pad: f32,
}

/// Desktop-dock geometry derived from the icon grid. `icon` uses
/// [`DOCK_ICON_FRACTION`] (smaller than the homescreen-grid
/// [`ICON_FRACTION`]) so dock icons sit in a tighter glyph footprint
/// inside the glass pill; `pill_pad` mirrors the legacy
/// `padding(10.0)` value the dock's `Container` applied before the
/// icon-driven `dock::DOCK_PAD_FRACTION` math took over (kept as a
/// fallback metric for callers that still want a single padding
/// number — the dock module itself derives padding from `icon`).
pub( crate ) fn dock_geom_desktop( screen_w: u32, screen_h: u32, dims: &GridDims ) -> DockGeomDesktop
{
	let icon = ( dims.cell_w * DOCK_ICON_FRACTION ).round().max( 40.0 * ltk::density().max( 1.0 ) );
	DockGeomDesktop
	{
		icon,
		spacing:  dp( 14.0, screen_w, screen_h ),
		pill_pad: dp( 10.0, screen_w, screen_h ),
	}
}

#[cfg(test)]
mod tests
{
	use super::*;

	// A few representative logical-pixel surfaces.
	const PHONE_W: u32 = 720;  const PHONE_H: u32 = 1440; // portrait
	const TALL_W:  u32 = 1080; const TALL_H:  u32 = 2340; // portrait, hi-res
	const DESK_W:  u32 = 1600; const DESK_H:  u32 = 900;  // landscape

	#[test]
	fn dp_is_passthrough_in_landscape()
	{
		// width > height ⇒ absolute pixels, no scaling.
		assert_eq!( dp( 88.0, DESK_W, DESK_H ), 88.0 );
	}

	#[test]
	fn dp_scales_linearly_in_portrait()
	{
		// At the reference unit (min dimension == SCREEN_UNIT_REF) dp is identity.
		assert!( ( dp( 88.0, SCREEN_UNIT_REF as u32, 960 ) - 88.0 ).abs() < 1e-3 );
		// Half the reference width ⇒ half the pixels.
		let half = SCREEN_UNIT_REF / 2.0;
		assert!( ( dp( 88.0, half as u32, 960 ) - 44.0 ).abs() < 1e-3 );
	}

	#[test]
	fn portrait_grid_has_five_columns_and_real_cells()
	{
		let g = grid_dims( PHONE_W, PHONE_H, false );
		assert_eq!( g.cols, MOBILE_COLS );
		assert!( g.rows >= 1 );
		assert!( g.cell_w > 0.0 );
		// Portrait cell stacks an icon over a label, so it is taller than wide.
		assert!( g.cell_h > g.cell_w );
		assert!( g.spacing_x >= 0.0 );
	}

	#[test]
	fn portrait_columns_fill_the_width()
	{
		// COLS·cell + (COLS+1)·gap should fill the screen width (edge inset
		// equals the inter-column gap), within rounding slack.
		for &( w, h ) in &[ ( PHONE_W, PHONE_H ), ( TALL_W, TALL_H ) ]
		{
			let g = grid_dims( w, h, false );
			let total = g.cols as f32 * g.cell_w
				+ ( g.cols as f32 + 1.0 ) * g.spacing_x;
			assert!(
				( total - w as f32 ).abs() <= 10.0,
				"width {w}: laid out {total}, expected ~{w}"
			);
		}
	}

	#[test]
	fn landscape_grid_is_well_formed()
	{
		let g = grid_dims( DESK_W, DESK_H, true );
		assert!( g.cols >= 2 );
		assert!( g.rows >= 1 );
		assert!( g.cell_w > 0.0 );
	}

	#[test]
	fn portrait_side_pad_equals_the_inter_column_gap()
	{
		// In portrait the edge inset is the same MOBILE_GAP_FRAC of the width
		// used between columns, so the five columns fill the row exactly.
		let pad = grid_side_pad( PHONE_W, PHONE_H, false );
		let gap = grid_dims( PHONE_W, PHONE_H, false ).spacing_x;
		assert!( ( pad - gap ).abs() < 1e-3 );
		assert!( pad > 0.0 );
	}
}
