//! Pointer/touch gestures on the switcher: tile drags to another screen,
//! the open/close swipe, and the mobile carousel drag with its inertia.

use super::*;

impl AppSwitcher {
    /// Returns the `ObjectId` of the selected entry and hides the overlay.
    /// See [`commit_object_id_index`] for why callers prefer the `ObjectId`
    /// form over a per-client `u32`.
    /// Grab the tile under `pos`, if any, for a possible cross-screen drag.
    /// Returns true when a tile was grabbed; the press is otherwise a plain
    /// click for [`Self::click_at`] to resolve.
    pub fn tile_drag_begin(&mut self, pos: smithay::utils::Point<f64, Logical>) -> bool {
        if !self.visible || self.is_hiding {
            return false;
        }
        let hit = self.tile_rects.iter().position(|r| {
            !r.is_empty() && r.to_f64().contains(pos)
        });
        let Some(entry) = hit else { return false };
        // A press on the close button is not a drag.
        if self
            .close_rects
            .get(entry)
            .is_some_and(|r| !r.is_empty() && r.to_f64().contains(pos))
        {
            return false;
        }
        let size = self.tile_rects[entry].size;
        self.tile_drag = Some(TileDrag { entry, start: pos, pos, size, moved: false });
        true
    }

    /// Track the grabbed tile. Returns true once past the slop, i.e. when
    /// the gesture has become a drag and the panel should repaint.
    pub fn tile_drag_motion(&mut self, pos: smithay::utils::Point<f64, Logical>) -> bool {
        let Some(drag) = self.tile_drag.as_mut() else { return false };
        drag.pos = pos;
        let dx = pos.x - drag.start.x;
        let dy = pos.y - drag.start.y;
        if !drag.moved && (dx * dx + dy * dy).sqrt() > TILE_DRAG_SLOP {
            drag.moved = true;
        }
        drag.moved
    }

    /// Finish the grab. `Some(entry)` when the tile actually travelled (the
    /// caller decides where to), `None` when it was a click in disguise.
    pub fn tile_drag_end(&mut self) -> Option<usize> {
        let drag = self.tile_drag.take()?;
        drag.moved.then_some(drag.entry)
    }


    /// Open the switcher in finger-driven entry mode: the panel forms and
    /// settles as [`set_swipe_progress`] follows the bottom swipe-up, in
    /// place of the timed fade. [`commit_swipe`] freezes it fully open;
    /// [`cancel_swipe`] tears it back down.
    pub fn begin_swipe(
        &mut self,
        entries: Vec<WindowEntry>,
        focused: Option<smithay::reexports::wayland_server::backend::ObjectId>,
    ) {
        self.begin(entries, 0, focused);
        if self.visible {
            self.swipe_progress = Some(0.0);
            self.fade_frames = 0;
        }
    }

    /// Drive the finger-following entry from a bottom-swipe progress
    /// `[0, 1]`. No-op once the entry has been committed or was never a
    /// swipe (Alt+Tab / hot-corner).
    pub fn set_swipe_progress(&mut self, progress: f32) {
        if self.swipe_progress.is_some() {
            self.swipe_progress = Some(progress.clamp(0.0, 1.0));
        }
    }

    /// Freeze the swipe-driven panel fully open and interactive — the
    /// dwell committed the switcher. The fade clock takes over at full
    /// alpha so later dismiss/commit animate normally.
    pub fn commit_swipe(&mut self) {
        if self.swipe_progress.take().is_some() {
            self.fade_frames = FADE_FRAMES_TOTAL;
            self.opened_at = Some(std::time::Instant::now());
        }
    }

    /// Abandon a swipe-driven entry (released below the commit threshold):
    /// drop the panel so the foreground app underneath takes over again.
    pub fn cancel_swipe(&mut self) {
        self.swipe_progress = None;
        self.cancel();
    }

    /// True while the panel is being formed by the bottom swipe-up.
    pub fn is_swiping(&self) -> bool {
        self.swipe_progress.is_some()
    }

    /// True while a carousel press→drag is in flight (used by the pointer
    /// path to feed mouse motion into the drag, since winit has no
    /// TouchMotion events).
    pub fn is_carousel_dragging(&self) -> bool {
        self.carousel_drag.is_some()
    }


    /// Start a touch/pointer drag on the mobile carousel. Tracked even
    /// when only one entry exists so a tap on its close button still
    /// reaches `carousel_drag_end` → `click_at`; horizontal motion is
    /// then clamped to zero in render so the lone tile never scrolls.
    pub fn carousel_drag_begin(
        &mut self,
        pos: smithay::utils::Point<f64, smithay::utils::Logical>,
    ) {
        if self.entries.is_empty() {
            return;
        }
        let now = std::time::Instant::now();
        self.carousel_drag = Some(CarouselDrag {
            start_pos: pos,
            last_pos: pos,
            last_time: now,
            start_offset: self.carousel_offset,
            moved_px: 0.0,
            velocity: 0.0,
            axis: CarouselAxis::Undecided,
            close_idx: None,
        });
        self.carousel_velocity = 0.0;
        self.carousel_snap = None;
        self.close_drag = None;
    }

    /// Hit-test `point` against the last-rendered tile rects, returning the
    /// tile index it lands on (skipping the zero-size off-screen tiles).
    fn tile_index_at(
        &self,
        point: smithay::utils::Point<f64, smithay::utils::Logical>,
    ) -> Option<usize> {
        let (px, py) = (point.x as i32, point.y as i32);
        self.tile_rects.iter().position(|r| {
            r.size.w > 0
                && r.size.h > 0
                && px >= r.loc.x
                && px < r.loc.x + r.size.w
                && py >= r.loc.y
                && py < r.loc.y + r.size.h
        })
    }

    /// Update the carousel from a touch/pointer motion sample. A horizontal
    /// drag pans the row; a vertical drag lifts the grabbed tile toward a
    /// close (mirrors the overview's swipe-up-to-close).
    pub fn carousel_drag_motion(
        &mut self,
        pos: smithay::utils::Point<f64, smithay::utils::Logical>,
    ) {
        let Some(d) = self.carousel_drag.as_mut() else {
            return;
        };
        let now = std::time::Instant::now();
        let dx = (pos.x - d.last_pos.x) as f32;
        let dy = (pos.y - d.last_pos.y) as f32;
        let dt = now.duration_since(d.last_time).as_secs_f32().max(0.001);
        // Low-pass the per-sample velocity so a noisy last sample doesn't
        // dominate the inertia launch.
        d.velocity = 0.7 * d.velocity + 0.3 * (dx / dt);
        d.moved_px += dx.abs() + dy.abs();
        d.last_pos = pos;
        d.last_time = now;
        let total_dx = (pos.x - d.start_pos.x) as f32;
        let total_dy = (pos.y - d.start_pos.y) as f32;
        if d.axis == CarouselAxis::Undecided && d.moved_px > CAROUSEL_AXIS_LOCK_PX {
            d.axis = if total_dy.abs() > total_dx.abs() {
                CarouselAxis::Vertical
            } else {
                CarouselAxis::Horizontal
            };
        }
        let axis = d.axis;
        let mut close_idx = d.close_idx;
        let start_pos = d.start_pos;
        let start_offset = d.start_offset;
        // Resolve the grabbed tile once, while the rects are still at rest —
        // re-hit-testing each frame against the lifting tile makes it flicker.
        if axis == CarouselAxis::Vertical && close_idx.is_none() {
            close_idx = self.tile_index_at(start_pos);
            if let Some(d) = self.carousel_drag.as_mut() {
                d.close_idx = close_idx;
            }
        }
        match axis {
            CarouselAxis::Vertical => {
                self.close_drag = close_idx.map(|i| (i, total_dy.min(0.0)));
            }
            _ => {
                self.close_drag = None;
                self.carousel_offset = start_offset + total_dx;
            }
        }
    }

    /// End the drag. Returns the hit-test result at the press position when
    /// the release was a tap (drag distance below the threshold) so the
    /// caller can route Activate / Close / Dismiss through its normal
    /// dispatch. Returns `None` when the gesture was a real drag — inertia
    /// then takes over.
    pub fn carousel_drag_end(&mut self) -> Option<AppSwitcherClick> {
        let Some(d) = self.carousel_drag.take() else {
            return None;
        };
        if d.moved_px <= CAROUSEL_TAP_PX {
            return Some(self.click_at(d.start_pos));
        }
        if d.axis == CarouselAxis::Vertical {
            let close = self.close_drag.take();
            return match close {
                Some((idx, dy)) if dy <= -CAROUSEL_CLOSE_COMMIT_PX => {
                    Some(AppSwitcherClick::Close(idx))
                }
                // Not lifted far enough: snap the tile back, close nothing.
                _ => None,
            };
        }
        self.carousel_velocity = d.velocity;
        self.carousel_last_tick = Some(std::time::Instant::now());
        None
    }

    /// Advance inertia / snap state by the wall-clock delta since the last
    /// tick. Called once per frame by the host while [`animating`] is true.
    pub fn tick_carousel(&mut self) {
        if self.carousel_drag.is_some() {
            return;
        }
        let now = std::time::Instant::now();
        let dt = self
            .carousel_last_tick
            .map(|t| now.duration_since(t).as_secs_f32().min(0.05))
            .unwrap_or(0.016);
        self.carousel_last_tick = Some(now);

        if self.carousel_velocity.abs() > CAROUSEL_SNAP_VELOCITY {
            self.carousel_offset += self.carousel_velocity * dt;
            let decay = (-CAROUSEL_FRICTION * dt).exp();
            self.carousel_velocity *= decay;
        } else if self.carousel_velocity.abs() > f32::EPSILON {
            // Below the snap threshold — flag the render to adopt the
            // nearest tile as the new selection, then let its snap-ease
            // finish positioning the offset.
            self.carousel_velocity = 0.0;
            self.carousel_last_tick = None;
            self.carousel_settling = true;
        }
    }
}
