//! Mobile homescreen paging.
//!
//! Owns the live `drag_dx` offset, the settle tween that springs it back
//! to zero after a commit or cancel, and the horizontal swipe handlers
//! that commit a page change when the user's finger crosses the per-page
//! threshold. The wallpaper parallax in [`crate::homescreen::Homescreen::view`]
//! derives from `current_page + drag_dx`, so every state transition here
//! is a visible frame in the renderer.

use crate::message::Message;
use crate::state::Crustace;

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

/// Asymptotic fraction of the screen width that an unbounded rubber-band
/// pull can reach. A determined user dragging arbitrarily far past the
/// first / last page sees the offset converge to
/// `screen_w * RUBBER_BAND_FRACTION` and never exceed it. Exposed so the
/// homescreen wallpaper can hold back exactly the same headroom on each
/// side and keep the rubber-band region painted with wallpaper pixels
/// instead of the black surface — the two values are linked by
/// definition, leaving them as separate magic numbers in two files would
/// be a regression waiting to happen.
pub( crate ) const RUBBER_BAND_FRACTION: f32 = 0.25;

/// Map an over-edge pull (positive logical pixels) into a rubber-banded
/// offset. Pure function — no shell state — so paging tests can pin the
/// curve.
///
/// The denominator grows linearly with `over`, so the absolute slope is
/// 1.0 at `over == 0` (the page tracks the finger flush) and approaches
/// `screen_w * RUBBER_BAND_FRACTION` as `over → ∞`. The asymptote means
/// a determined user can never pull the offset past
/// [`RUBBER_BAND_FRACTION`] of the screen width past the edge — enough
/// for a visible "you reached the end" hint, not enough to expose a
/// non-existent neighbour.
fn rubber_band_past( over: f32, screen_w: f32 ) -> f32
{
	if over <= 0.0 || screen_w <= 0.0 { return 0.0; }
	// `c` is the reciprocal of the asymptote fraction — picking
	// `c = 4` (= 1/0.25) reaches the 0.25 sw asymptote at
	// `over ≈ sw`, which feels like iOS without the user ever needing
	// to drag more than a screen-width past the edge to see it.
	let c = 1.0 / RUBBER_BAND_FRACTION;
	let denom = 1.0 + ( over / screen_w ) * c;
	over / denom
}

/// Time it takes for a committed page swipe (or cancel) to settle back to
/// rest. Short enough to feel responsive on a phone, long enough for the
/// eye to register the slide. Matches the iOS page-control feel.
pub( crate ) const PAGE_SLIDE_MS: u64 = 180;

/// Settle time for tap-driven page jumps via the page indicator. Slower
/// than [`PAGE_SLIDE_MS`] so a multi-page jump (where intermediate pages
/// flash through the screen) reads as a deliberate slide rather than a
/// snap, and so the user can confirm visually that the dot they tapped
/// landed on the right page. The delta does not feed into the duration:
/// a 4-page jump still uses the same window — the eased curve covers the
/// full distance, intermediate pages just streak by faster. Same shape
/// as iOS / Android tap-to-scroll-to-top.
pub( crate ) const PAGE_JUMP_MS:  u64 = 320;

/// In-flight settle tween that slides `drag_dx` back to zero after a page
/// commit or a drag cancel. Ltk's `is_animating()` polls this so the run
/// loop rebuilds the view every frame until the animation completes.
pub( crate ) struct PageAnim
{
	pub start_time: Instant,
	pub start_dx:   f32,
	pub duration:   Duration,
}

impl Crustace
{
	/// Start (or restart) the settle tween that animates `drag_dx` back to
	/// zero. Called after every page commit and after a cancelled drag so
	/// the wallpaper visibly springs to rest instead of snapping.
	pub( crate ) fn start_page_settle( &mut self )
	{
		if self.drag_dx.abs() < 0.5
		{
			self.drag_dx          = 0.0;
			self.page_anim        = None;
			if self.pages_sub_active { self.pages_exit_handoff = 3; }
			self.pages_sub_active = false;
			return;
		}
		self.page_anim = Some( PageAnim {
			start_time: Instant::now(),
			start_dx:   self.drag_dx,
			duration:   Duration::from_millis( PAGE_SLIDE_MS ),
		} );
	}

	/// Advance the homescreen page-settle tween by one renderer tick.
	/// Ease-out cubic interpolation from `start_dx` → 0 over `duration`;
	/// done in one pass and cleared when the tween finishes so
	/// `is_animating()` stops flapping.
	pub( crate ) fn tick_page_anim( &mut self )
	{
		let Some( ref anim ) = self.page_anim else { return };
		let elapsed = Instant::now().saturating_duration_since( anim.start_time );
		let t       = ( elapsed.as_secs_f32() / anim.duration.as_secs_f32() ).clamp( 0.0, 1.0 );
		let eased   = 1.0 - ( 1.0 - t ).powi( 3 );
		self.drag_dx = anim.start_dx * ( 1.0 - eased );
		if t >= 1.0
		{
			self.drag_dx          = 0.0;
			self.page_anim        = None;
			if self.pages_sub_active { self.pages_exit_handoff = 3; }
			self.pages_sub_active = false;
		}
	}

	/// Advance the page-subsurface enter handoff by one renderer tick. The
	/// interactive grid keeps painting on the main for these frames while the
	/// sliding subsurfaces raster and map, so the handoff does not flash an
	/// iconless wallpaper. Mirror of the launcher's reveal handoff.
	pub( crate ) fn tick_pages_handoff( &mut self )
	{
		if self.pages_enter_handoff > 0 { self.pages_enter_handoff -= 1; }
		if self.pages_exit_handoff  > 0 { self.pages_exit_handoff  -= 1; }
	}

	/// Cancel any in-flight pager state. Called when a drag starts: a drag and a
	/// page swipe are mutually exclusive, and a horizontal sample taken just
	/// before the drag armed can leave `pages_sub_active` set — which would hide
	/// the main grid behind a stale page subsurface for the rest of the drag.
	pub( crate ) fn cancel_paging( &mut self )
	{
		self.drag_dx             = 0.0;
		self.page_anim           = None;
		self.pages_sub_active    = false;
		self.pages_enter_handoff = 0;
		self.pages_exit_handoff  = 0;
	}

	/// `true` while the main should paint the interactive grid: at rest, and
	/// through the first two handoff frames of a drag. During those the main is
	/// frozen (motion-only) and just keeps its at-rest raster — which already
	/// has the grid — so the opaque subsurfaces have time to raster and cover
	/// without the main re-rastering. The grid only drops (one grid-less
	/// re-raster, at `enter == 1`) so a rubber-band over-pull reveals clean
	/// wallpaper, not the grid.
	pub( crate ) fn pages_grid_on_main( &self ) -> bool
	{
		!self.pages_sub_active || self.pages_enter_handoff >= 2
	}

	/// `true` while the sliding page subsurfaces should be emitted — during a
	/// finger drag, its settle, and the brief exit handoff that holds them over
	/// the main while it re-rasters the grid back.
	pub( crate ) fn pages_subsurface_shown( &self ) -> bool
	{
		self.pages_sub_active || self.pages_exit_handoff > 0
	}

	/// `true` whenever the main is frozen during a page slide — i.e. the whole
	/// drag and settle, EXCEPT the single `enter == 1` frame that re-rasters the
	/// main grid-less before the freeze. At the start the main simply keeps its
	/// at-rest raster (grid intact) while the subsurfaces come up, so only that
	/// one grid-less frame costs a main re-raster instead of the whole handoff.
	pub( crate ) fn pages_subsurface_motion_only( &self ) -> bool
	{
		self.pages_sub_active && self.pages_enter_handoff != 1
	}

	/// Clamp the configured swipe threshold to a sane band so a typo in the
	/// user's toml can't lock swipes from ever committing or commit every
	/// tiny flick twice.
	pub( crate ) fn swipe_horizontal_threshold( &self ) -> f32
	{
		self.config.shell.swipe_threshold.clamp( 0.1, 0.9 )
	}

	/// Follow a horizontal-swipe progress sample. Past the first / last
	/// page the swipe rubber-bands instead of hard-clamping: the offset
	/// follows the finger with quadratic dampening, capped at ~25 % of
	/// the screen width, and the settle tween springs it back to zero
	/// on release. Matches the iOS / Android homescreen edge feel and
	/// gives a clear "you've reached the end" signal without being able
	/// to pull a non-existent neighbour into frame.
	pub( crate ) fn handle_swipe_horizontal_progress( &mut self, progress: f32 )
	{
		// Only mobile has multiple pages; desktop ignores the gesture.
		if !self.use_mobile_layout() { return; }

		// ltk routes `on_swipe_horizontal_progress` for a gesture on ANY surface,
		// so a horizontal sample while the launcher is open (e.g. the start of a
		// launcher-icon drag) or while an item is being dragged would otherwise
		// activate the homescreen pager and leave `pages_sub_active` stuck —
		// hiding the main grid behind a stale page subsurface. Neither is a page
		// change.
		if self.launcher.is_shown() || self.dragging_item.is_some() { return; }

		// Progress is `dx / (screen_width * threshold)` in physical px,
		// so convert back to a logical-pixel offset. On scale != 1 the
		// result is off by the scale factor, matching the rest of the
		// gesture paths in this shell.
		let sw = self.screen_width as f32;
		if sw <= 0.0 { return; }
		let threshold = self.swipe_horizontal_threshold();
		let raw       = progress * sw * threshold;

		let pages     = self.config.mobile.pages.len();
		let last_page = pages.saturating_sub( 1 );
		let on_first  = self.current_page == 0;
		let on_last   = self.current_page >= last_page;
		let max_dx    = if on_first { 0.0 } else {  sw };
		let min_dx    = if on_last  { 0.0 } else { -sw };

		let target = if raw > max_dx
		{
			rubber_band_past( raw - max_dx, sw ) + max_dx
		}
		else if raw < min_dx
		{
			-rubber_band_past( min_dx - raw, sw ) + min_dx
		}
		else
		{
			raw
		};

		// ltk fires `progress(0.0)` when the user releases without
		// crossing the commit threshold. Kick off a settle animation
		// instead of snapping back instantly so the page visibly
		// springs to rest — this is also what cancels a rubber-banded
		// over-pull and bounces it back to flush.
		if progress == 0.0 && self.drag_dx.abs() > 0.5
		{
			self.start_page_settle();
			return;
		}

		// Coalesce to ~vsync once the slide is live: the touchscreen samples far
		// faster than the compositor presents, and one reposition-commit per
		// sample floods the wayland socket on the Librem5 until a flush blocks
		// and the drag freezes. A sample within the window is dropped — the next
		// one past it applies the latest position.
		let now = std::time::Instant::now();
		if self.pages_sub_active
			&& self.pages_drag_last_applied.is_some_and( |t| now.duration_since( t ) < std::time::Duration::from_millis( 15 ) )
		{
			return;
		}
		self.pages_drag_last_applied = Some( now );

		self.page_anim = None;
		self.drag_dx   = target;
		// First live sample of a drag: hand the pages off to the sliding
		// subsurfaces. Each page subsurface is opaque (its own wallpaper slice +
		// icons), so it covers the frozen main and the wallpaper moves 1:1 with
		// the icons. The enter handoff drops the main grid (grid-less re-raster)
		// before the main freezes so a rubber-band over-pull reveals clean
		// wallpaper, not a duplicate grid.
		if !self.pages_sub_active
		{
			self.pages_sub_active    = true;
			self.pages_enter_handoff = 3;
			self.pages_exit_handoff  = 0;
			// Force a full build of the window on the first frame — the
			// subsurfaces were dropped after the previous drag.
			self.pages_last_center.set( None );
		}
	}

	/// Commit a left-swipe: advance to the next page if one exists. The
	/// new page visually stays where it was at release (by shifting
	/// `drag_dx` by +screen_width) and the settle tween animates it back
	/// to centre.
	pub( crate ) fn handle_swipe_left( &mut self ) -> Option<Message>
	{
		if !self.use_mobile_layout() { return None; }
		let pages = self.config.mobile.pages.len();
		if pages == 0 { return None; }
		let sw = self.screen_width as f32;

		if self.current_page + 1 < pages
		{
			self.current_page += 1;
			self.drag_dx += sw;
		}
		self.start_page_settle();
		None
	}

	/// Commit a right-swipe: retreat to the previous page. Symmetric to
	/// [`handle_swipe_left`]; at page 0 only the settle tween runs.
	pub( crate ) fn handle_swipe_right( &mut self ) -> Option<Message>
	{
		if !self.use_mobile_layout() { return None; }
		if self.current_page == 0
		{
			self.start_page_settle();
			return None;
		}
		let sw = self.screen_width as f32;
		self.current_page -= 1;
		self.drag_dx -= sw;
		self.start_page_settle();
		None
	}

	/// Jump directly to `target` (used by page-indicator taps).
	///
	/// Same shape as [`handle_swipe_left`] generalised over distance:
	/// after committing the new page, shift `drag_dx` by
	/// `(target - current) * sw` so the pre-jump page stays visually
	/// centred on the first frame, then ease the offset back to zero
	/// to slide the new page in. The sign convention matches the
	/// swipe handlers — a forward jump (e.g. 0 → 2) leaves `drag_dx`
	/// positive (`+2 sw`) so the settle tween animates it down to
	/// zero, sliding pages from right to left exactly like two
	/// stacked swipe-left commits would. The previous version had the
	/// sign flipped (`current - target`), which made the slide go in
	/// the opposite direction of the tapped dot.
	///
	/// Inlines the settle bookkeeping instead of calling
	/// `start_page_settle` so the tap can use the longer
	/// [`PAGE_JUMP_MS`] window — single-page swipes still spring back
	/// at [`PAGE_SLIDE_MS`].
	pub( crate ) fn jump_to_page( &mut self, target: usize )
	{
		if !self.use_mobile_layout() { return; }
		let pages = self.config.mobile.pages.len();
		if pages == 0 || target >= pages { return; }
		if target == self.current_page { return; }
		let sw    = self.screen_width as f32;
		let delta = target as f32 - self.current_page as f32;
		self.current_page = target;
		self.drag_dx     += delta * sw;
		if self.drag_dx.abs() < 0.5
		{
			self.drag_dx   = 0.0;
			self.page_anim = None;
			return;
		}
		self.page_anim = Some( PageAnim {
			start_time: Instant::now(),
			start_dx:   self.drag_dx,
			duration:   Duration::from_millis( PAGE_JUMP_MS ),
		} );
	}
}
