//! Drag origin tagging + drop-target hit-testing.
//!
//! All coordinate work that decides "where would this drop land?" lives
//! here, isolated from the renderer in [`super::view`]. The geometry
//! values feeding these tests come from [`super::geometry`], which is
//! also what the renderer reads — same `(cols, rows, cell, spacing*)`
//! flow into both, so a hit lands in exactly the cell the user can see.
//!
//! All coordinates are in **physical pixels**. ltk delivers `on_drop`
//! and pointer events in physical px; `on_resize` reports the surface
//! size in the same space.

use super::geometry::{
	dock_band, dock_geom_desktop, dp, grid_dims, grid_top_reserve,
	page_indicator, GridDims, MOBILE_COLS,
};

/// Where a drag originates from. Used by the shell to know what to remove
/// when a drop is accepted, and to stamp the edit-mode context menu.
///
/// The launcher variant carries no payload because launcher cells are
/// ephemeral — a launcher drag only ever adds an item to the dock / grid.
/// The dock and grid variants carry `index` (the slot into the `Vec` that
/// owns the item) rather than cell coordinates: Vec::remove is a direct
/// index op, and cell coordinates can shift when items are appended.
#[ derive( Debug, Clone ) ]
pub enum DragOrigin
{
	Launcher,
	Dock        { index: usize },
	Running     { app_id: String },
	GridMobile  { page: usize, index: usize },
	GridDesktop { index: usize },
}

/// Where a drag-from-launcher drop lands on the homescreen.
///
/// Coarse banding: the bottom strip is the dock, everything above is the
/// grid. `Grid` carries the `(col, row)` computed from the same layout the
/// renderer uses. `None` covers margins and misses that should cancel the
/// drop silently (outside the visible grid / dock area).
#[ derive( Debug, Clone, Copy, PartialEq ) ]
pub enum DropTarget
{
	Grid { col: u32, row: u32 },
	Dock,
	None,
}

/// Hit-test a drop point against the homescreen's grid / dock bands.
///
/// `x`, `y`, `screen_w` and `screen_h` are all in **physical pixels**.
/// Returns `DropTarget::None` if the point lands in neither band, which
/// the caller should treat as a silent cancel.
pub fn drop_target( screen_w: u32, screen_h: u32, dock_items_count: usize, x: f32, y: f32 ) -> DropTarget
{
	if screen_w == 0 || screen_h == 0 { return DropTarget::None; }
	let is_landscape = screen_w > screen_h;

	// Dock band — the bottom strip where the favorites row lives. The band
	// metrics live in [`dock_band`] so [`grid_dims`] can cap rows against
	// the same dock top, preventing the grid from extending under it.
	let ( dock_top_y, _ ) = dock_band( screen_w, screen_h, is_landscape );

	// Grid band — fixed top reserve then the `(cols, rows)` grid with
	// `cell` pixel cells, `spacing_x` horizontal gap and `spacing_y`
	// vertical gap. All three match what `view::build_grid` hands to
	// `WrapGrid::layout`, so every pixel is round-trip consistent.
	let dims        = grid_dims( screen_w, screen_h, is_landscape );
	let GridDims { cols, rows, cell_w, cell_h, spacing_x, spacing_y } = dims;
	let grid_w      = cols as f32 * cell_w + ( cols as f32 - 1.0 ) * spacing_x;
	let grid_left   = ( screen_w as f32 - grid_w ).max( 0.0 ) * 0.5;
	let pitch_y     = cell_h + spacing_y;
	let top_reserve = grid_top_reserve( screen_w, screen_h, is_landscape );

	if y >= dock_top_y && y <= screen_h as f32
	{
		// On desktop the dock is a centred pill that does not span the
		// full width: dropping in the bottom strip but to the side of
		// the pill should NOT land in the dock. Mobile / portrait keeps
		// the legacy behaviour because the dock there occupies the full
		// width anyway.
		if is_landscape
		{
			let ( pill_left, pill_right ) = desktop_dock_pill_x_bounds( screen_w, screen_h, dock_items_count );
			if x < pill_left || x > pill_right { return DropTarget::None; }
		}
		return DropTarget::Dock;
	}
	// Grid hit-band: top_reserve → grid_bottom_y. In mobile portrait
	// the grid stops `page_indicator(...).band` above the dock so the
	// indicator strip is not a drop target — drops there fall through
	// to `DropTarget::None` rather than landing on the bottom row of
	// icons that the user can no longer see at that y. With spacing_y
	// distributing the slack the grid reaches `grid_bottom_y` flush,
	// so any point in this strip maps to a real cell via row-pitch
	// arithmetic. Horizontally we clamp to `grid_w` — the side margins
	// are genuine empty space.
	let indicator_reserve = if is_landscape { 0.0 } else { page_indicator( screen_w, screen_h ).band };
	let grid_bottom_y     = dock_top_y - indicator_reserve;
	if x >= grid_left && x < grid_left + grid_w && y >= top_reserve && y < grid_bottom_y
	{
		let col_f = ( x - grid_left ) / ( cell_w + spacing_x );
		let row_f = ( y - top_reserve ) / pitch_y;
		let col   = ( col_f.floor().max( 0.0 ) as u32 ).min( cols.saturating_sub( 1 ) );
		let row   = ( row_f.floor().max( 0.0 ) as u32 ).min( rows.saturating_sub( 1 ) );
		return DropTarget::Grid { col, row };
	}
	// Intentionally ignore `side_pad` for the grid-left computation — the
	// column layout is `align_center_x`, which centres the *container*'s
	// preferred size (grid + 2 * side_pad) in the surface. On narrow
	// portrait grids the container often hits the surface width, so the
	// effective grid-left is just `(screen_w - grid_w) / 2`; on wider
	// landscapes the same formula is off by at most `side_pad`, which is
	// safely inside cell bounds.
	DropTarget::None
}

/// Maximum number of dock items (excluding the launcher button) for a
/// given surface. Mobile is hard-capped at 4 so icons remain
/// finger-sized in portrait; desktop's cap is whatever keeps the
/// dock pill no wider than the icon grid above — the visual reads as
/// "the dock is anchored to the edges of the icon grid" and a drop
/// that would push the pill past the grid edge is rejected.
///
/// The pill width formula matches what [`crate::dock::build_dock`]
/// paints: `(items + 1) * icon + items * spacing + 2 * pill_pad`.
/// Solving for `items` ≤ `(grid_w - icon - 2 * pill_pad) / (icon + spacing)`.
pub fn dock_capacity( screen_w: u32, screen_h: u32, is_landscape: bool ) -> usize
{
	if !is_landscape { return MOBILE_COLS as usize; }
	let dims     = grid_dims( screen_w, screen_h, is_landscape );
	let geom     = dock_geom_desktop( screen_w, screen_h, &dims );
	let grid_w   = dims.cols as f32 * dims.cell_w + ( dims.cols as f32 - 1.0 ) * dims.spacing_x;
	let max_n    = ( grid_w - geom.icon - 2.0 * geom.pill_pad ) / ( geom.icon + geom.spacing );
	( max_n.floor().max( 1.0 ) ) as usize
}

/// Horizontal bounds (left_x, right_x) of the desktop dock pill in physical
/// pixels. Mirrors the pill width formula in [`crate::dock::build_dock`]:
/// launcher button + `dock_items_count` favourites, plus icon-spacing gaps
/// and the pill's outer padding. Used both by [`drop_target`] (to reject
/// drops in the bottom strip but outside the pill) and by the cursor-shape
/// feedback that flips to `NoDrop` over the same dead zone.
pub fn desktop_dock_pill_x_bounds( screen_w: u32, screen_h: u32, dock_items_count: usize ) -> ( f32, f32 )
{
	let dims  = grid_dims( screen_w, screen_h, true );
	let geom  = dock_geom_desktop( screen_w, screen_h, &dims );
	let count = dock_items_count + 1;
	let pill_w = count as f32 * geom.icon
		+ ( count.saturating_sub( 1 ) as f32 ) * geom.spacing
		+ 2.0 * geom.pill_pad;
	let left  = ( screen_w as f32 - pill_w ) * 0.5;
	( left, left + pill_w )
}

/// Compute the drop-preview slot index for a desktop dock drag, or
/// `None` if the cursor is not over the dock band, or accepting the
/// drop would push the dock pill past the icon grid's width.
///
/// The slot index `k ∈ [0, items_count]` means "insert before
/// `dock_items[k]`" (or append at the tail when `k == items_count`).
/// Computed against the *current* (pre-preview) layout — the visual
/// gap that opens up shifts the row laterally, but the snap point
/// stays anchored to where the cursor entered the band.
///
/// `adds_item` is `false` for a Dock → Dock reorder (the slot count
/// doesn't change) and `true` for any drag that would grow the dock
/// (Launcher → Dock, Grid → Dock). Only the latter triggers the
/// width guard.
pub fn dock_preview_index(
	cursor_x:    f32,
	cursor_y:    f32,
	screen_w:    u32,
	screen_h:    u32,
	items_count: usize,
	adds_item:   bool,
	is_landscape: bool,
) -> Option<usize>
{
	if !is_landscape { return None; }
	let ( dock_top_y, _ ) = dock_band( screen_w, screen_h, is_landscape );
	if cursor_y < dock_top_y { return None; }

	// X-bounds guard — only suggest a preview slot when the cursor is
	// actually over the dock pill. Dropping in the bottom strip but to
	// the side of the pill is a cancel ([`drop_target`] returns `None`
	// there), so the dock should not visually open a slot for it either:
	// otherwise the user sees the row part and assumes the drop landed
	// even though it did not.
	let ( pill_left, pill_right ) = desktop_dock_pill_x_bounds( screen_w, screen_h, items_count );
	if cursor_x < pill_left || cursor_x > pill_right { return None; }

	let dims = grid_dims( screen_w, screen_h, is_landscape );
	let geom = dock_geom_desktop( screen_w, screen_h, &dims );

	// Width guard — only enforced for drags that grow the dock.
	if adds_item
	{
		let grid_w     = dims.cols as f32 * dims.cell_w + ( dims.cols as f32 - 1.0 ) * dims.spacing_x;
		let proposed_n = items_count + 1;
		let proposed_w = ( proposed_n + 1 ) as f32 * geom.icon
			+ proposed_n as f32 * geom.spacing
			+ 2.0 * geom.pill_pad;
		if proposed_w > grid_w { return None; }
	}

	// Geometry of the *current* row (no preview slot). One launcher
	// + `items_count` favourites = `count` icons total.
	let count        = items_count + 1;
	let row_w        = count as f32 * geom.icon + ( count.saturating_sub( 1 ) as f32 ) * geom.spacing;
	let row_left     = ( screen_w as f32 - row_w ) * 0.5;
	let first_item_x = row_left + geom.icon + geom.spacing;
	let slot_w       = geom.icon + geom.spacing;
	// Map the cursor to the nearest gap. `relative + slot_w/2`
	// shifts the boundary so the gap "snaps" at the midpoint between
	// adjacent icons — same convention as `dock_reorder`'s `raw.round()`
	// but expressed without Rust's bankers-rounding.
	let relative = cursor_x - first_item_x + slot_w * 0.5;
	let gap      = ( relative / slot_w ).floor().max( 0.0 ) as usize;
	Some( gap.min( items_count ) )
}

/// Approximate on-screen centre (in logical pixels) of the dock icon at
/// position `dock_index` in the dock's linear item list. Used as the anchor
/// point for the edit-mode context menu so it appears near the icon the
/// long-press originated on. Mirrors [`crate::dock::build_dock`]'s icon
/// sizes / spacing plus the leftmost launcher button slot.
pub fn dock_item_anchor( dock_index: usize, dock_len: usize, screen_w: u32, screen_h: u32 ) -> ( f32, f32 )
{
	let is_landscape = screen_w > screen_h;
	let icon    = dp( if is_landscape { 40.0 } else { 52.0 }, screen_w, screen_h );
	let spacing = dp( if is_landscape { 14.0 } else { 20.0 }, screen_w, screen_h );
	// `+1` accounts for the launcher button pinned at the left of the row.
	let count   = dock_len + 1;
	let row_w   = count as f32 * icon + ( count.saturating_sub( 1 ) as f32 ) * spacing;
	let row_left = ( screen_w as f32 - row_w ) * 0.5;
	let x = row_left + ( dock_index + 1 ) as f32 * ( icon + spacing ) + icon * 0.5 - spacing;
	// Vertically: centre of the dock strip.
	let ( dock_top, dock_bot ) = dock_band( screen_w, screen_h, is_landscape );
	let y = ( dock_top + dock_bot ) * 0.5;
	( x, y )
}

pub fn dock_temp_anchor( temp_idx: usize, pinned_count: usize, temps_count: usize, screen_w: u32, screen_h: u32 ) -> ( f32, f32 )
{
	let is_landscape = screen_w > screen_h;
	let icon      = dp( if is_landscape { 40.0 } else { 52.0 }, screen_w, screen_h );
	let spacing   = dp( if is_landscape { 14.0 } else { 20.0 }, screen_w, screen_h );
	let pinned_total = pinned_count + 1;
	let total_slots  = pinned_total + temps_count;
	let row_w   = total_slots as f32 * icon + ( total_slots.saturating_sub( 1 ) as f32 ) * spacing;
	let row_left = ( screen_w as f32 - row_w ) * 0.5;
	let slot_idx = pinned_total + temp_idx;
	let x = row_left + slot_idx as f32 * ( icon + spacing ) + icon * 0.5;
	let ( dock_top, dock_bot ) = dock_band( screen_w, screen_h, is_landscape );
	let y = ( dock_top + dock_bot ) * 0.5;
	( x, y )
}

/// Approximate on-screen centre (in logical pixels) of the grid cell at
/// `(col, row)` for the current surface. Used as the anchor for the edit
/// menu — mirrors the exact geometry [`drop_target`] uses in reverse so
/// menu placement and hit-testing share a single source of truth.
pub fn grid_cell_anchor( col: u32, row: u32, screen_w: u32, screen_h: u32 ) -> ( f32, f32 )
{
	let is_landscape = screen_w > screen_h;
	let dims     = grid_dims( screen_w, screen_h, is_landscape );
	let GridDims { cols, cell_w, cell_h, spacing_x, spacing_y, .. } = dims;
	let grid_w   = cols as f32 * cell_w + ( cols as f32 - 1.0 ) * spacing_x;
	let grid_left = ( screen_w as f32 - grid_w ).max( 0.0 ) * 0.5;
	let x = grid_left + col as f32 * ( cell_w + spacing_x ) + cell_w * 0.5;
	let y = grid_top_reserve( screen_w, screen_h, is_landscape ) + row as f32 * ( cell_h + spacing_y ) + cell_h * 0.5;
	( x, y )
}
