//! Swipe gesture state machines used by `Forge`: the top-bar swipe-down and the
//! bottom-edge swipe-up, with their progress/commit thresholds and unit tests.

/// Tracked state for an in-progress topbar swipe-down.
/// Becomes `active` once the pointer has moved past
/// `SWIPE_START_THRESHOLD` below the starting point; until then it's
/// still ambiguous between a tap and a swipe.
#[derive(Debug, Clone)]
pub struct TopbarSwipe {
    pub start_y: f64,
    pub active: bool,
    pub last_sent_progress: f32,
    /// Drag length mapping to progress 1.0, derived from the output height
    /// at arm time (see [`TopbarSwipe::full_distance_for`]); the fixed 300
    /// was half the screen on a 600-logical-tall output.
    pub full_distance: f64,
}

/// Message emitted by the topbar swipe state machine, corresponding
/// one-to-one with the IPC payloads forge sends to crustace.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TopbarSwipeMsg {
    Start,
    Wiping(f32),
    Done { progress: f32, committed: bool },
}

impl TopbarSwipe {
    pub const START_THRESHOLD: f64 = 6.0;
    pub const FULL_DISTANCE: f64 = 300.0;
    pub const PROGRESS_STEP: f32 = 0.015;
    pub const COMMIT_THRESHOLD: f32 = 0.5;

    /// Full-progress drag length for an output `height` logical px tall:
    /// 40% of the height, floored at 120 so tiny outputs keep a real
    /// gesture, capped at the canonical [`Self::FULL_DISTANCE`] so normal
    /// screens keep today's feel.
    pub fn full_distance_for(height: f64) -> f64 {
        (height * 0.4).clamp(120.0, Self::FULL_DISTANCE)
    }

    #[cfg(test)]
    pub fn armed(start_y: f64) -> Self {
        Self {
            start_y,
            active: false,
            last_sent_progress: -1.0,
            full_distance: Self::FULL_DISTANCE,
        }
    }

    /// Consume a motion event. Returns up to two messages: `Start`
    /// on the first activation, then a `Wiping(progress)` if the
    /// progress has moved past `PROGRESS_STEP` since the last emit.
    /// Returns `(consumed, start?, wiping?)`: `consumed` is true if
    /// the state machine owns the event and normal input handling
    /// should be skipped.
    pub fn on_motion(
        &mut self,
        cur_y: f64,
    ) -> (bool, Option<TopbarSwipeMsg>, Option<TopbarSwipeMsg>) {
        let dy = cur_y - self.start_y;

        let mut start_msg = None;
        if !self.active {
            if dy < Self::START_THRESHOLD {
                return (false, None, None);
            }
            self.active = true;
            self.last_sent_progress = 0.0;
            start_msg = Some(TopbarSwipeMsg::Start);
        }

        let progress = (dy / self.full_distance).clamp(0.0, 1.0) as f32;
        let wiping_msg = if (progress - self.last_sent_progress).abs() >= Self::PROGRESS_STEP
            || (progress - 1.0).abs() < f32::EPSILON
        {
            self.last_sent_progress = progress;
            Some(TopbarSwipeMsg::Wiping(progress))
        } else {
            None
        };

        (true, start_msg, wiping_msg)
    }

    /// Consume a release event. Returns the `Done` message if the
    /// swipe had activated, `None` if it was still armed but never
    /// escalated (caller should let normal tap handling run).
    pub fn on_release(self, cur_y: f64) -> Option<TopbarSwipeMsg> {
        if !self.active {
            return None;
        }
        let dy = cur_y - self.start_y;
        let progress = (dy / self.full_distance).clamp(0.0, 1.0) as f32;
        let committed = progress >= Self::COMMIT_THRESHOLD;
        Some(TopbarSwipeMsg::Done {
            progress,
            committed,
        })
    }
}

/// Which surface the bottom swipe-up acts on, decided when the gesture
/// activates: `Home` drives the crustace launcher reveal over IPC, `App`
/// shrinks the foreground window toward the homescreen.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SwipeContext {
    /// Homescreen, swipe starting at the dock's top edge or higher: drives
    /// crustace's app-launcher reveal.
    Launcher,
    /// Bottom swipe-up that forms the app switcher — from the very bottom
    /// on the homescreen (minimised apps) or from the bottom band over an
    /// app. Over an app it also enables the Android go-home outcome.
    Switcher,
}

/// Tracked state for an in-progress bottom edge swipe-up — the
/// Android-style home / launcher / recents gesture. Armed on every
/// qualifying press; only `active` once upward travel passes
/// `START_THRESHOLD` with the vertical axis dominant.
#[derive(Debug, Clone)]
pub struct BottomSwipe {
    pub start_x: f64,
    pub start_y: f64,
    pub avail_h: f64,
    pub active: bool,
    pub context: Option<SwipeContext>,
    /// Set at activation: the swipe started over a foreground app (not the
    /// bare homescreen), so releasing past `HOME_COMMIT` goes home.
    pub over_app: bool,
    pub last_sent_progress: f32,
    /// Set once the gesture resolves early — the dwell commits the app
    /// switcher, or a swipe-up dismisses an open panel. Remaining motion is
    /// consumed inertly and the release does nothing more.
    pub resolved: bool,
}

impl BottomSwipe {
    pub const START_THRESHOLD: f64 = 6.0;
    pub const PROGRESS_STEP: f32 = 0.015;
    pub const LAUNCHER_COMMIT: f32 = 0.25;
    pub const HOME_COMMIT: f32 = 0.5;
    pub const DWELL_ARM: f32 = 0.25;

    pub fn armed(start_x: f64, start_y: f64, avail_h: f64) -> Self {
        Self {
            start_x,
            start_y,
            avail_h: avail_h.max(1.0),
            active: false,
            context: None,
            over_app: false,
            last_sent_progress: -1.0,
            resolved: false,
        }
    }

    /// Upward travel as a fraction `[0, 1]` of the available height.
    pub fn progress(&self, cur_y: f64) -> f32 {
        ((self.start_y - cur_y) / self.avail_h).clamp(0.0, 1.0) as f32
    }

    /// Consume a motion event. Returns `(consumed, activated, wiping?)`:
    /// `consumed` means the gesture owns the event (skip normal input);
    /// `activated` is true only on the frame it escalates past the
    /// threshold; `wiping` carries a fresh progress when it moved past
    /// `PROGRESS_STEP`.
    pub fn on_motion(&mut self, cur_x: f64, cur_y: f64) -> (bool, bool, Option<f32>) {
        let dy = self.start_y - cur_y;

        let mut activated = false;
        if !self.active {
            if dy < Self::START_THRESHOLD {
                return (false, false, None);
            }
            // Axis lock: a horizontal-dominant drag is left to paging / back.
            if (cur_x - self.start_x).abs() > dy {
                return (false, false, None);
            }
            self.active = true;
            self.last_sent_progress = 0.0;
            activated = true;
        }

        let progress = self.progress(cur_y);
        // Emit on the activation frame too: at the START_THRESHOLD the progress
        // is well below PROGRESS_STEP, so without this the reveal does not begin
        // tracking the finger until ~PROGRESS_STEP of travel — a visible dead
        // zone before the launcher starts to come out.
        let wiping = if activated
            || (progress - self.last_sent_progress).abs() >= Self::PROGRESS_STEP
            || (progress - 1.0).abs() < f32::EPSILON
        {
            self.last_sent_progress = progress;
            Some(progress)
        } else {
            None
        };

        (true, activated, wiping)
    }
}

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

    #[test]
    fn tap_does_not_activate() {
        let mut s = TopbarSwipe::armed(10.0);
        let (consumed, start, wiping) = s.on_motion(11.0);
        assert!(!consumed);
        assert!(start.is_none());
        assert!(wiping.is_none());
        assert!(!s.active);
        assert!(s.on_release(11.0).is_none());
    }

    #[test]
    fn crosses_threshold_fires_start_and_wiping() {
        let mut s = TopbarSwipe::armed(0.0);
        let (consumed, start, wiping) = s.on_motion(8.0);
        assert!(consumed);
        assert_eq!(start, Some(TopbarSwipeMsg::Start));
        match wiping {
            Some(TopbarSwipeMsg::Wiping(p)) => {
                assert!((p - 8.0 / 300.0).abs() < 1e-4);
            }
            other => panic!("expected wiping, got {:?}", other),
        }
        assert!(s.active);
    }

    #[test]
    fn wiping_is_throttled() {
        let mut s = TopbarSwipe::armed(0.0);
        s.on_motion(8.0); // activate
                          // Tiny jitter below PROGRESS_STEP (0.015 * 300 = 4.5 px).
        let (consumed, _, wiping) = s.on_motion(10.0);
        assert!(consumed);
        assert!(wiping.is_none(), "jitter must not emit a new wiping");

        // Beyond the step: emit.
        let (_, _, wiping) = s.on_motion(20.0);
        assert!(matches!(wiping, Some(TopbarSwipeMsg::Wiping(_))));
    }

    #[test]
    fn full_travel_clamps_to_one_and_emits() {
        let mut s = TopbarSwipe::armed(0.0);
        s.on_motion(8.0);
        let (_, _, wiping) = s.on_motion(500.0);
        match wiping {
            Some(TopbarSwipeMsg::Wiping(p)) => assert!((p - 1.0).abs() < 1e-4),
            other => panic!("expected wiping=1.0, got {:?}", other),
        }
    }

    #[test]
    fn release_below_commit_threshold_not_committed() {
        let mut s = TopbarSwipe::armed(0.0);
        s.on_motion(10.0); // activate, progress ≈ 0.033
        let msg = s.on_release(100.0).unwrap();
        match msg {
            TopbarSwipeMsg::Done {
                progress,
                committed,
            } => {
                assert!((progress - 100.0 / 300.0).abs() < 1e-4);
                assert!(!committed);
            }
            _ => panic!("expected Done"),
        }
    }

    #[test]
    fn release_past_commit_threshold_committed() {
        let mut s = TopbarSwipe::armed(0.0);
        s.on_motion(10.0);
        let msg = s.on_release(200.0).unwrap();
        match msg {
            TopbarSwipeMsg::Done {
                progress,
                committed,
            } => {
                assert!((progress - 200.0 / 300.0).abs() < 1e-4);
                assert!(committed);
            }
            _ => panic!("expected Done"),
        }
    }

    #[test]
    fn upward_motion_never_activates() {
        let mut s = TopbarSwipe::armed(50.0);
        let (consumed, _, _) = s.on_motion(0.0);
        assert!(!consumed);
        assert!(!s.active);
    }
}
