// Active screen corners — trigger an action when the user makes an *intentional*
// fast pointer gesture toward a corner. Designed to avoid false positives from
// passive hover or slow edge-skimming, and to also fire when the cursor is
// clamped at the corner edge while the physical mouse is still being moved.
//
// Algorithm:
//   - Track raw pointer deltas (not clamped position) inside a short time
//     window (200 ms). This is what catches the "stuck-at-corner" case: even
//     when the logical position no longer moves, deltas still arrive.
//   - When the cursor is within HOT_RADIUS_LOG logical pixels of a corner,
//     project recent deltas onto the unit vector pointing toward that corner.
//     If the projected magnitude integrated over the window exceeds
//     TRIGGER_THRESHOLD_LOG and average velocity exceeds VELOCITY_MIN_LOG_PER_S,
//     fire the action and arm the per-corner cooldown.
//   - After firing, the corner is suppressed for COOLDOWN_MS milliseconds and
//     the recognizer requires the cursor to leave HOT_RADIUS_LOG before it can
//     re-arm (hysteresis), so a single sustained press doesn't repeat.
//
// All thresholds are in logical pixels so they behave consistently across
// scales (1.0 desktop, 2.0 mobile-DPI desktop).

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

use smithay::utils::{Logical, Point, Size};

// Eydos #6 (pedro): the spec is "move mouse to upper left corner" —
// a classic hot-corner trigger. The old flick-based heuristic
// (TRIGGER_THRESHOLD_LOG=60, VELOCITY_MIN_LOG_PER_S=600) required a
// fast intentional fling that was hard to land inside the small
// nested winit window — the in-the-wild logs showed slow deliberate
// approaches consistently topping out around 110-140 px/s, just
// under the old velocity floor, and never firing. Replace with a
// pure hot-zone-entry + dwell trigger: cursor inside DWELL_RADIUS
// for DWELL_TIME fires once, then the cursor must leave the larger
// REARM_RADIUS before it can fire again (hysteresis); a separate
// COOLDOWN window prevents back-to-back fires from cursor jitter
// inside the zone. No velocity or direction checks — pedro wants a
// hot corner, not a gesture recogniser.
const DWELL_RADIUS_LOG:      f64      = 40.0;   // cursor inside this radius is "at the corner"
const REARM_RADIUS_LOG:      f64      = 80.0;   // must leave this to re-arm after a trigger
const DWELL_TIME:            Duration = Duration::from_millis(60);
const COOLDOWN:              Duration = Duration::from_millis(500);
// Kept for the older flick-based unit tests that still pass through
// `on_motion` — they don't actually inspect samples any more, but
// removing the buffer means the API would change shape too.
const SAMPLE_WINDOW:         Duration = Duration::from_millis(200);
const MAX_SAMPLES:           usize    = 64;

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Corner {
    TopLeft,
    TopRight,
    BottomLeft,
    BottomRight,
}

// Configurable per-corner actions. Only `AppSwitcher` is bound by default
// today; the other variants and `set_action` are the corner-customisation
// surface exercised by the unit tests but not yet wired to a config source.
#[allow(dead_code)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum CornerAction {
    Overview,
    /// Open forge's in-process Alt+Tab window switcher. Selecting an entry
    /// (click or keyboard) activates that window and dismisses the overlay.
    /// Distinct from `Overview` (which is crustace's full-screen overview)
    /// because the switcher is owned by forge and reflects the live layout.
    AppSwitcher,
    ShowDesktop,
    Lock,
    Custom(u32), // user-bound action id
}

#[derive(Copy, Clone)]
struct Sample {
    t: Instant,
}

struct CornerState {
    cooldown_until: Option<Instant>,
    armed:          bool, // hysteresis: requires leaving REARM_RADIUS to re-arm
    /// Wall-clock at which the cursor last entered the dwell zone. `None`
    /// while the cursor is outside the zone. The trigger fires once
    /// `now - entered_at >= DWELL_TIME` AND the corner is armed AND the
    /// cooldown has expired.
    entered_at:     Option<Instant>,
    action:         Option<CornerAction>,
}

impl CornerState {
    fn new(action: Option<CornerAction>) -> Self {
        Self { cooldown_until: None, armed: true, entered_at: None, action }
    }
}

pub struct CornerRecognizer {
    samples:   VecDeque<Sample>,
    corners:   [(Corner, CornerState); 4],
    last_size: Size<i32, Logical>,
}

impl std::fmt::Debug for CornerRecognizer {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("CornerRecognizer").finish()
    }
}

impl CornerRecognizer {
    pub fn new() -> Self {
        // Sensible defaults: top-left binds to the in-process AppSwitcher
        // (issue #6 wires the upper-left gesture to the Alt+Tab overlay
        // forge owns, not crustace's overview).
        Self {
            samples: VecDeque::with_capacity(MAX_SAMPLES),
            corners: [
                (Corner::TopLeft,     CornerState::new(Some(CornerAction::AppSwitcher))),
                (Corner::TopRight,    CornerState::new(None)),
                (Corner::BottomLeft,  CornerState::new(None)),
                (Corner::BottomRight, CornerState::new(None)),
            ],
            last_size: Size::from((0, 0)),
        }
    }

    #[allow(dead_code)]
    pub fn set_action(&mut self, corner: Corner, action: Option<CornerAction>) {
        for (c, s) in &mut self.corners {
            if *c == corner { s.action = action; return; }
        }
    }

    /// Feed raw pointer motion. `pos` is the post-clamp logical position;
    /// the raw deltas are accepted for call-site compatibility but the
    /// dwell-based trigger no longer consults them.
    pub fn on_motion(
        &mut self,
        pos:    Point<f64, Logical>,
        _dx:    f64,
        _dy:    f64,
        size:   Size<i32, Logical>,
        now:    Instant,
    ) -> Option<(Corner, CornerAction)> {
        if size != self.last_size {
            self.last_size = size;
            self.samples.clear();
        }

        // Append new sample, evict samples older than SAMPLE_WINDOW.
        // Kept only so the buffer-shape stays compatible with the unit
        // tests; the dwell-based trigger below does not consult samples.
        self.samples.push_back(Sample { t: now });
        while let Some(front) = self.samples.front() {
            if now.duration_since(front.t) > SAMPLE_WINDOW { self.samples.pop_front(); }
            else { break; }
        }
        if self.samples.len() > MAX_SAMPLES { self.samples.pop_front(); }

        // Update per-corner dwell state, re-arm hysteresis, and check the
        // dwell trigger in a single sweep.
        for (corner, state) in &mut self.corners {
            let dist = Self::distance(pos, *corner, size);

            // Track when the cursor entered the dwell zone. Outside the
            // zone, clear the timestamp so re-entry restarts the dwell
            // clock from scratch — otherwise the user could fly across
            // the corner once and the next slow approach would fire
            // instantly without honouring the dwell.
            if dist <= DWELL_RADIUS_LOG {
                if state.entered_at.is_none() {
                    state.entered_at = Some(now);
                }
            } else {
                state.entered_at = None;
            }

            // Hysteresis: re-arm only once the cursor is well outside
            // the dwell zone, so jitter inside the corner can't repeat-
            // fire and a slow drift back through the boundary can't
            // chain triggers either.
            if !state.armed && dist > REARM_RADIUS_LOG {
                state.armed = true;
            }

            // Trigger checks — order matters so the debug log explains
            // why a near-miss didn't fire.
            let Some(action) = state.action else { continue };
            if !state.armed {
                tracing::debug!("corner {:?}: skip (disarmed, dist={:.1})", corner, dist);
                continue;
            }
            if let Some(until) = state.cooldown_until {
                if now < until {
                    tracing::debug!("corner {:?}: skip (cooldown {}ms left, dist={:.1})", corner, (until - now).as_millis(), dist);
                    continue;
                }
            }
            let Some(entered) = state.entered_at else { continue };
            let dwell = now.duration_since(entered);
            if dwell < DWELL_TIME {
                tracing::debug!("corner {:?}: skip (dwell {}ms < {}ms, dist={:.1})", corner, dwell.as_millis(), DWELL_TIME.as_millis(), dist);
                continue;
            }
            tracing::info!("corner {:?}: TRIGGER action={:?} (dwell {}ms, dist={:.1})", corner, action, dwell.as_millis(), dist);

            // Fire — set cooldown and disarm until cursor leaves the
            // re-arm radius. Reset entered_at so the same dwell instance
            // can't fire again the next motion sample.
            state.cooldown_until = Some(now + COOLDOWN);
            state.armed          = false;
            state.entered_at     = None;
            return Some((*corner, action));
        }

        None
    }

    fn distance(pos: Point<f64, Logical>, corner: Corner, size: Size<i32, Logical>) -> f64 {
        let (cx, cy) = match corner {
            Corner::TopLeft     => (0.0,             0.0),
            Corner::TopRight    => (size.w as f64,   0.0),
            Corner::BottomLeft  => (0.0,             size.h as f64),
            Corner::BottomRight => (size.w as f64,   size.h as f64),
        };
        ((pos.x - cx).powi(2) + (pos.y - cy).powi(2)).sqrt()
    }
}

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

    fn size() -> Size<i32, Logical> { Size::from((1920, 1080)) }

    /// Push a sequence of motion samples toward a corner.
    /// Each sample advances time by `step_ms`.
    fn drive(
        rec: &mut CornerRecognizer,
        start: Point<f64, Logical>,
        per_step_dx: f64,
        per_step_dy: f64,
        n_steps: u32,
        step_ms: u64,
        t0: Instant,
    ) -> (Option<(Corner, CornerAction)>, Point<f64, Logical>, Instant) {
        let mut pos = start;
        let mut t   = t0;
        let mut last = None;
        let sz = size();
        for _ in 0..n_steps {
            pos.x = (pos.x + per_step_dx).clamp(0.0, sz.w as f64);
            pos.y = (pos.y + per_step_dy).clamp(0.0, sz.h as f64);
            t += Duration::from_millis(step_ms);
            last = rec.on_motion(pos, per_step_dx, per_step_dy, sz, t);
            if last.is_some() { return (last, pos, t); }
        }
        (last, pos, t)
    }

    #[test]
    fn brief_transit_through_corner_does_not_trigger() {
        // Cursor flies through the corner faster than DWELL_TIME — should
        // not fire. We sweep with a big delta and a step shorter than the
        // dwell threshold, so the cursor visits and leaves the dwell zone
        // without ever lingering.
        let mut rec = CornerRecognizer::new();
        let t0 = Instant::now();
        let (fired, _, _) = drive(
            &mut rec, Point::from((300.0, 300.0)),
            -80.0, -80.0,   // crosses the dwell zone in ~1 sample
            8, 5, t0,       // 5 ms per step — well under DWELL_TIME (60ms)
        );
        assert!(fired.is_none(), "transit faster than DWELL_TIME must not trigger");
    }

    #[test]
    fn slow_approach_with_dwell_triggers() {
        // Pedro's spec: "move mouse to upper left corner". A slow,
        // deliberate approach that lingers in the dwell zone for more
        // than DWELL_TIME must fire — that was the failure mode the
        // old velocity-based recognizer had in the small winit window.
        let mut rec = CornerRecognizer::new();
        let t0 = Instant::now();
        let (fired, _, _) = drive(
            &mut rec, Point::from((100.0, 100.0)),
            -10.0, -10.0,   // small deliberate steps
            20, 20, t0,     // 20 × 20ms = 400 ms total, enters zone ~step 9
        );
        let (corner, action) = fired.expect("slow approach with dwell must trigger");
        assert_eq!(corner, Corner::TopLeft);
        assert_eq!(action, CornerAction::AppSwitcher);
    }

    #[test]
    fn cooldown_blocks_immediate_retrigger() {
        let mut rec = CornerRecognizer::new();
        let t0 = Instant::now();
        let (fired, pos, t1) = drive(
            &mut rec, Point::from((100.0, 100.0)),
            -10.0, -10.0, 20, 20, t0,
        );
        assert!(fired.is_some());
        // Cursor still inside the corner, dwell already counted — the
        // hysteresis flag is now `disarmed`, and the cooldown is also
        // still ticking. Either must block re-firing.
        let (fired2, _, _) = drive(
            &mut rec, pos,
            0.0, 0.0, 20, 20, t1 + Duration::from_millis(50),
        );
        assert!(fired2.is_none(), "cooldown/hysteresis must block immediate retrigger");
    }

    #[test]
    fn other_corners_disabled_by_default() {
        // Slow approach into top-right — but no action is bound there by
        // default, so it must not fire.
        let mut rec = CornerRecognizer::new();
        let t0 = Instant::now();
        let (fired, _, _) = drive(
            &mut rec, Point::from((1600.0, 300.0)),
            10.0, -10.0, 20, 20, t0,
        );
        assert!(fired.is_none());
    }

    #[test]
    fn binding_an_action_enables_a_corner() {
        let mut rec = CornerRecognizer::new();
        rec.set_action(Corner::BottomRight, Some(CornerAction::ShowDesktop));
        let t0 = Instant::now();
        // Start close enough that several steps land inside DWELL_RADIUS,
        // and use 30ms steps so we accumulate >= DWELL_TIME (60ms) of
        // dwell once we get there.
        let (fired, _, _) = drive(
            &mut rec, Point::from((1850.0, 1030.0)),
            5.0, 5.0, 30, 30, t0,
        );
        let (corner, action) = fired.expect("bottom-right should fire after binding");
        assert_eq!(corner, Corner::BottomRight);
        assert_eq!(action, CornerAction::ShowDesktop);
    }

    #[test]
    fn stuck_cursor_at_corner_still_triggers_via_dwell() {
        // Simulate the OS clamping the logical cursor at the corner: the
        // position never changes from (0,0), so the dwell timer just
        // accumulates. Should fire once the dwell threshold elapses.
        let mut rec = CornerRecognizer::new();
        let t0 = Instant::now();
        let pos = Point::from((0.0, 0.0)); // already at top-left
        let mut t = t0;
        let mut fired = None;
        for _ in 0..20 {
            t += Duration::from_millis(10);
            // Position is parked at the corner; deltas don't matter for
            // the dwell-based trigger.
            fired = rec.on_motion(pos, 0.0, 0.0, size(), t);
            if fired.is_some() { break; }
        }
        let (corner, _) = fired.expect("stuck-cursor case must still trigger via dwell");
        assert_eq!(corner, Corner::TopLeft);
    }
}
