// Active screen corners — trigger an action when the user *pushes* the
// pointer into a corner: the cursor must already be pinned at the vertex
// (the compositor clamps it there) and the raw motion deltas must keep
// travelling toward the corner. Passing by, or touching the corner and
// stopping, never fires; a deliberate push through it does, without any
// velocity requirement. After firing, the corner stays disarmed until the
// cursor leaves the re-arm radius, and a short cooldown absorbs jitter.
//
// Thresholds are in logical pixels so they behave the same across scales;
// `push_distance` and `pin_radius` come from `forge.toml -> [corners]`.

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

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

pub const DEFAULT_PUSH_DISTANCE: f64 = 80.0;
pub const DEFAULT_PIN_RADIUS: f64 = 4.0;
const REARM_RADIUS_LOG: f64 = 80.0;
const COOLDOWN: Duration = Duration::from_millis(500);
/// A pause longer than this while pinned forgets the push so far, so a
/// tap on the corner followed by a later twitch cannot add up.
const PUSH_IDLE: Duration = Duration::from_millis(250);

#[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
}

struct CornerState {
    cooldown_until: Option<Instant>,
    armed: bool,
    /// Push accumulated toward the vertex while pinned, in logical pixels.
    push: f64,
    last_push_at: Option<Instant>,
    action: Option<CornerAction>,
}

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

pub struct CornerRecognizer {
    corners: [(Corner, CornerState); 4],
    push_distance: f64,
    pin_radius: f64,
}

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 {
            corners: [
                (Corner::TopLeft,     CornerState::new(Some(CornerAction::AppSwitcher))),
                (Corner::TopRight,    CornerState::new(None)),
                (Corner::BottomLeft,  CornerState::new(None)),
                (Corner::BottomRight, CornerState::new(None)),
            ],
            push_distance: DEFAULT_PUSH_DISTANCE,
            pin_radius: DEFAULT_PIN_RADIUS,
        }
    }

    pub fn set_thresholds(&mut self, push_distance: f64, pin_radius: f64) {
        self.push_distance = push_distance.max(1.0);
        self.pin_radius = pin_radius.max(0.5);
    }

    #[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; }
        }
    }

    /// Fraction of the push completed at the corner the cursor is pinned
    /// to (0..1), for on-screen feedback. `None` when no bound, armed
    /// corner is being pushed.
    pub fn progress(&self) -> Option<(Corner, f64)> {
        self.corners
            .iter()
            .find(|(_, s)| s.action.is_some() && s.armed && s.push > 0.0)
            .map(|(c, s)| (*c, (s.push / self.push_distance).min(1.0)))
    }

    /// Feed raw pointer motion: `pos` is the post-clamp logical position
    /// on the screen of `size`, `dx`/`dy` the raw deltas, which keep
    /// arriving while the cursor is pinned at an edge.
    pub fn on_motion(
        &mut self,
        pos: Point<f64, Logical>,
        dx: f64,
        dy: f64,
        size: Size<i32, Logical>,
        now: Instant,
    ) -> Option<(Corner, CornerAction)> {
        for (corner, state) in &mut self.corners {
            let dist = Self::distance(pos, *corner, size);

            if !state.armed && dist > REARM_RADIUS_LOG {
                state.armed = true;
            }

            let pinned = dist <= self.pin_radius;
            let idle = state
                .last_push_at
                .is_some_and(|t| now.duration_since(t) > PUSH_IDLE);
            if !pinned || idle {
                state.push = 0.0;
                state.last_push_at = None;
            }
            if !pinned {
                continue;
            }

            let toward = Self::push_toward(*corner, dx, dy);
            if toward > 0.0 {
                state.push += toward;
                state.last_push_at = Some(now);
            }

            let Some(action) = state.action else { continue };
            if !state.armed {
                continue;
            }
            if state.cooldown_until.is_some_and(|until| now < until) {
                continue;
            }
            if state.push < self.push_distance {
                continue;
            }
            tracing::info!("corner {:?}: TRIGGER action={:?} (push {:.1} px)", corner, action, state.push);

            state.cooldown_until = Some(now + COOLDOWN);
            state.armed = false;
            state.push = 0.0;
            state.last_push_at = None;
            return Some((*corner, action));
        }

        None
    }

    /// Motion components pointing into the corner, summed: pushing along
    /// either edge counts, so a diagonal and an axis-aligned shove behave
    /// alike once the cursor is pinned.
    fn push_toward(corner: Corner, dx: f64, dy: f64) -> f64 {
        let (sx, sy) = match corner {
            Corner::TopLeft     => (-dx, -dy),
            Corner::TopRight    => (dx, -dy),
            Corner::BottomLeft  => (-dx, dy),
            Corner::BottomRight => (dx, dy),
        };
        sx.max(0.0) + sy.max(0.0)
    }

    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; the position is clamped to the
    /// screen like the compositor does, the deltas are passed raw.
    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 passing_near_the_corner_does_not_trigger() {
        let mut rec = CornerRecognizer::new();
        let (fired, _, _) = drive(&mut rec, Point::from((300.0, 60.0)), -40.0, 0.0, 20, 10, Instant::now());
        assert!(fired.is_none(), "skimming along the top edge must not fire");
    }

    #[test]
    fn touching_the_corner_and_stopping_does_not_trigger() {
        let mut rec = CornerRecognizer::new();
        let t0 = Instant::now();
        // Arrive at the vertex with a small overshoot, then rest there.
        let (fired, pos, t1) = drive(&mut rec, Point::from((30.0, 30.0)), -10.0, -10.0, 3, 10, t0);
        assert!(fired.is_none());
        assert_eq!(pos, Point::from((0.0, 0.0)));
        let (fired, _, _) = drive(&mut rec, pos, 0.0, 0.0, 30, 20, t1);
        assert!(fired.is_none(), "resting on the corner must not fire");
    }

    #[test]
    fn pushing_into_the_corner_triggers() {
        let mut rec = CornerRecognizer::new();
        let (fired, _, _) = drive(&mut rec, Point::from((20.0, 20.0)), -10.0, -10.0, 30, 10, Instant::now());
        let (corner, action) = fired.expect("a sustained push into the corner must fire");
        assert_eq!(corner, Corner::TopLeft);
        assert_eq!(action, CornerAction::AppSwitcher);
    }

    #[test]
    fn push_along_one_edge_counts() {
        let mut rec = CornerRecognizer::new();
        let (fired, _, _) = drive(&mut rec, Point::from((0.0, 0.0)), -5.0, 0.0, 30, 10, Instant::now());
        assert!(fired.is_some(), "shoving left while pinned in the corner must fire");
    }

    #[test]
    fn push_forgets_after_a_pause() {
        let mut rec = CornerRecognizer::new();
        let t0 = Instant::now();
        let (fired, pos, t1) = drive(&mut rec, Point::from((0.0, 0.0)), -5.0, -5.0, 6, 10, t0);
        assert!(fired.is_none());
        let t2 = t1 + Duration::from_millis(400);
        let (fired, _, _) = drive(&mut rec, pos, -5.0, -5.0, 6, 10, t2);
        assert!(fired.is_none(), "pushes separated by a pause must not add up");
    }

    #[test]
    fn progress_reports_the_partial_push() {
        let mut rec = CornerRecognizer::new();
        let (fired, _, _) = drive(&mut rec, Point::from((0.0, 0.0)), -5.0, -5.0, 2, 10, Instant::now());
        assert!(fired.is_none());
        let (corner, p) = rec.progress().expect("a partial push is reported");
        assert_eq!(corner, Corner::TopLeft);
        assert!((p - 20.0 / DEFAULT_PUSH_DISTANCE).abs() < 1e-9);
    }

    #[test]
    fn cooldown_and_hysteresis_block_immediate_retrigger() {
        let mut rec = CornerRecognizer::new();
        let t0 = Instant::now();
        let (fired, pos, t1) = drive(&mut rec, Point::from((0.0, 0.0)), -10.0, -10.0, 10, 10, t0);
        assert!(fired.is_some());
        let (fired2, _, _) = drive(&mut rec, pos, -10.0, -10.0, 20, 10, t1);
        assert!(fired2.is_none(), "must not fire again without leaving the corner");
    }

    #[test]
    fn rearms_after_leaving_the_corner() {
        let mut rec = CornerRecognizer::new();
        let t0 = Instant::now();
        let (fired, _, t1) = drive(&mut rec, Point::from((0.0, 0.0)), -10.0, -10.0, 10, 10, t0);
        assert!(fired.is_some());
        let (_, pos, t2) = drive(&mut rec, Point::from((0.0, 0.0)), 20.0, 20.0, 10, 10, t1 + Duration::from_secs(1));
        assert!(pos.x > REARM_RADIUS_LOG);
        let (fired, _, _) = drive(&mut rec, pos, -40.0, -40.0, 20, 10, t2);
        assert!(fired.is_some(), "a fresh push after re-arming must fire");
    }

    #[test]
    fn other_corners_disabled_by_default() {
        let mut rec = CornerRecognizer::new();
        let (fired, _, _) = drive(&mut rec, Point::from((1900.0, 20.0)), 10.0, -10.0, 20, 10, Instant::now());
        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 (fired, _, _) = drive(&mut rec, Point::from((1900.0, 1060.0)), 10.0, 10.0, 20, 10, Instant::now());
        let (corner, action) = fired.expect("bottom-right should fire after binding");
        assert_eq!(corner, Corner::BottomRight);
        assert_eq!(action, CornerAction::ShowDesktop);
    }

    #[test]
    fn thresholds_are_configurable() {
        let mut rec = CornerRecognizer::new();
        rec.set_thresholds(400.0, 4.0);
        let (fired, _, _) = drive(&mut rec, Point::from((0.0, 0.0)), -5.0, -5.0, 10, 10, Instant::now());
        assert!(fired.is_none(), "100 px of push must not reach a 400 px threshold");
    }
}
